lfapi

view lfapi/main.js @ 13:7708205ce4c5

Removed already completed todo
author bsw
date Tue Nov 01 11:40:58 2011 +0100 (2011-11-01)
parents 63ca0147e776
children fe484d4ca81f
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 if (!params.snapshot) {
654 query.from('interest');
655 } else if (params.snapshot == 'latest') {
656 query.from('direct_interest_snapshot', 'interest');
657 query.addWhere('interest.event = issue.latest_snapshot_event');
658 };
659 query.addField('interest.*');
660 query.join('member', null, 'member.id = interest.member_id');
661 query.join('issue', null, 'interest.issue_id = issue.id');
662 query.join('policy', null, 'policy.id = issue.policy_id');
663 query.join('area', null, 'area.id = issue.area_id');
664 query.join('unit', null, 'area.unit_id = unit.id');
665 general_params.addMemberOptions(req, query, params);
666 general_params.addIssueOptions(req, query, params);
667 query.addOrderBy('interest.issue_id, interest.member_id');
668 general_params.addLimitAndOffset(query, params);
669 db.query(conn, req, res, query, function (interest_result, conn) {
670 var result = { result: interest_result.rows }
671 includes = [];
672 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
673 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
674 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
675 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
676 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
677 addRelatedData(conn, req, res, result, includes);
678 });
679 });
680 },
682 '/issue_comment': function (conn, req, res, params) {
683 requireAccessLevel(conn, req, res, 'pseudonym', function() {
684 var query = new selector.Selector();
685 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');
686 query.addField('issue_comment.*');
687 general_params.addMemberOptions(req, query, params);
688 general_params.addIssueOptions(req, query, params);
689 query.addOrderBy('issue_comment.issue_id, issue_comment.member_id');
690 general_params.addLimitAndOffset(query, params);
691 db.query(conn, req, res, query, function (issue_comment_result, conn) {
692 var result = { result: issue_comment_result.rows }
693 includes = [];
694 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
695 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
696 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
697 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
698 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
699 addRelatedData(conn, req, res, result, includes);
700 });
701 });
702 },
704 '/initiative': function (conn, req, res, params) {
705 requireAccessLevel(conn, req, res, 'anonymous', function() {
706 var query = new selector.Selector();
707 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');
708 fields.addObjectFields(query, 'initiative');
709 query.addOrderBy('initiative.id');
710 general_params.addInitiativeOptions(req, query, params);
711 general_params.addLimitAndOffset(query, params);
712 db.query(conn, req, res, query, function (initiative_result, conn) {
713 var result = { result: initiative_result.rows }
714 includes = [];
715 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
716 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
717 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
718 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
719 addRelatedData(conn, req, res, result, includes);
720 });
721 });
722 },
724 '/initiator': function (conn, req, res, params) {
725 requireAccessLevel(conn, req, res, 'pseudonym', function() {
726 var fields = ['initiator.initiative_id', 'initiator.member_id'];
727 var query = new selector.Selector();
728 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');
729 query.addWhere('initiator.accepted');
730 fields.forEach( function(field) {
731 query.addField(field, null, ['grouped']);
732 });
733 general_params.addMemberOptions(req, query, params);
734 general_params.addInitiativeOptions(req, query, params);
735 query.addOrderBy('initiator.initiative_id, initiator.member_id');
736 general_params.addLimitAndOffset(query, params);
737 db.query(conn, req, res, query, function (initiator, conn) {
738 var result = { result: initiator.rows }
739 includes = [];
740 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
741 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
742 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
743 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
744 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
745 addRelatedData(conn, req, res, result, includes);
746 });
747 });
748 },
751 '/supporter': function (conn, req, res, params) {
752 requireAccessLevel(conn, req, res, 'pseudonym', function() {
753 var fields = ['supporter.issue_id', 'supporter.initiative_id', 'supporter.member_id', 'supporter.draft_id'];
754 var query = new selector.Selector();
755 query.from('supporter')
756 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');
757 fields.forEach( function(field) {
758 query.addField(field, null, ['grouped']);
759 });
760 general_params.addMemberOptions(req, query, params);
761 general_params.addInitiativeOptions(req, query, params);
762 query.addOrderBy('supporter.issue_id, supporter.initiative_id, supporter.member_id');
763 general_params.addLimitAndOffset(query, params);
764 db.query(conn, req, res, query, function (supporter, conn) {
765 var result = { result: supporter.rows }
766 includes = [];
767 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
768 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
769 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
770 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
771 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
772 addRelatedData(conn, req, res, result, includes);
773 });
774 });
775 },
777 '/battle': function (conn, req, res, params) {
778 requireAccessLevel(conn, req, res, 'anonymous', function() {
779 var query = new selector.Selector();
780 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');
781 query.addField('battle.*');
782 general_params.addInitiativeOptions(req, query, params);
783 query.addOrderBy('battle.issue_id, battle.winning_initiative_id, battle.losing_initiative_id');
784 general_params.addLimitAndOffset(query, params);
785 db.query(conn, req, res, query, function (result, conn) {
786 var result = { result: result.rows }
787 includes = [];
788 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
789 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
790 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
791 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
792 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
793 addRelatedData(conn, req, res, result, includes);
794 });
795 });
796 },
798 '/draft': function (conn, req, res, params) {
799 requireAccessLevel(conn, req, res, 'anonymous', function() {
800 var fields = ['draft.initiative_id', 'draft.id', 'draft.formatting_engine', 'draft.content', 'draft.author_id'];
801 var query = new selector.Selector();
802 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');
803 fields.forEach( function(field) {
804 query.addField(field, null, ['grouped']);
805 });
806 if (req.current_access_level != 'anonymous' || req.current_member_id) {
807 query.addField('draft.author_id');
808 }
809 if (params.draft_id) {
810 query.addWhere('draft.id = ?', params.draft_id);
811 }
812 if (params.current_draft) {
813 query.join('current_draft', null, 'current_draft.initiative_id = initiative.id AND current_draft.id = draft.id')
814 }
815 general_params.addInitiativeOptions(req, query, params);
816 query.addOrderBy('draft.initiative_id, draft.id');
817 general_params.addLimitAndOffset(query, params);
818 db.query(conn, req, res, query, function (result, conn) {
819 var result = { result: result.rows }
820 includes = [];
821 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
822 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
823 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
824 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
825 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
826 addRelatedData(conn, req, res, result, includes);
827 });
828 });
829 },
831 '/suggestion': function (conn, req, res, params) {
832 requireAccessLevel(conn, req, res, 'anonymous', function() {
833 var query = new selector.Selector();
834 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');
835 if (req.current_access_level == 'anonymous' && !req.current_member_id ) {
836 fields.addObjectFields(query, 'suggestion', 'suggestion_pseudonym');
837 } else {
838 fields.addObjectFields(query, 'suggestion');
839 }
840 general_params.addSuggestionOptions(req, query, params);
841 query.addOrderBy('suggestion.initiative_id, suggestion.id');
842 general_params.addLimitAndOffset(query, params);
843 db.query(conn, req, res, query, function (result, conn) {
844 var result = { result: result.rows }
845 includes = [];
846 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
847 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
848 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
849 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
850 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
851 addRelatedData(conn, req, res, result, includes);
852 });
853 });
854 },
856 '/opinion': function (conn, req, res, params) {
857 requireAccessLevel(conn, req, res, 'pseudonym', function() {
858 var fields = ['opinion.initiative_id', 'opinion.suggestion_id', 'opinion.member_id', 'opinion.degree', 'opinion.fulfilled']
859 var query = new selector.Selector();
860 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');
861 fields.forEach( function(field) {
862 query.addField(field, null, ['grouped']);
863 });
864 general_params.addMemberOptions(req, query, params);
865 general_params.addSuggestionOptions(req, query, params);
866 query.addOrderBy = ['opinion.initiative_id, opinion.suggestion_id, opinion.member_id'];
867 general_params.addLimitAndOffset(query, params);
868 db.query(conn, req, res, query, function (result, conn) {
869 var result = { result: result.rows }
870 includes = [];
871 if (params.include_suggestions) includes.push({ class: 'suggestion', objects: 'result'});
872 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'suggestions'});
873 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
874 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
875 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
876 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
877 addRelatedData(conn, req, res, result, includes);
878 });
879 });
880 },
882 '/delegation': function (conn, req, res, params) {
883 requireAccessLevel(conn, req, res, 'pseudonym', function() {
884 var fields = ['delegation.id', 'delegation.truster_id', 'delegation.trustee_id', 'delegation.scope', 'delegation.area_id', 'delegation.issue_id', 'delegation.unit_id'];
885 var query = new selector.Selector();
886 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');
887 fields.forEach( function(field) {
888 query.addField(field, null, ['grouped']);
889 });
890 if (params.direction) {
891 switch (params.direction) {
892 case 'in':
893 query.join('member', null, 'member.id = delegation.trustee_id');
894 break;
895 case 'out':
896 query.join('member', null, 'member.id = delegation.truster_id');
897 break;
898 default:
899 respond('json', conn, req, res, 'unprocessable', 'Direction must be "in" or "out" if set.');
900 }
901 } else {
902 query.join('member', null, 'member.id = delegation.truster_id OR member.id = delegation.trustee_id');
903 }
904 general_params.addMemberOptions(req, query, params);
905 general_params.addIssueOptions(req, query, params);
906 if (params.scope) {
907 query.addWhere(['delegation.scope IN (??)', params.scope.split(',')]);
908 };
909 query.addOrderBy = ['delegation.id'];
910 general_params.addLimitAndOffset(query, params);
911 db.query(conn, req, res, query, function (result, conn) {
912 respond('json', conn, req, res, 'ok', { result: result.rows });
913 });
914 });
915 },
917 '/vote': function (conn, req, res, params) {
918 requireAccessLevel(conn, req, res, 'pseudonym', function() {
919 var query = new selector.Selector();
920 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');
921 query.addField('vote.*');
922 query.addWhere('issue.closed_at NOTNULL');
923 general_params.addMemberOptions(req, query, params);
924 general_params.addInitiativeOptions(req, query, params);
925 general_params.addLimitAndOffset(query, params);
926 db.query(conn, req, res, query, function (result, conn) {
927 respond('json', conn, req, res, 'ok', { result: result.rows });
928 });
929 });
930 },
932 '/event': function (conn, req, res, params) { requireAccessLevel(conn, req, res, 'anonymous', function() {
933 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'];
934 var query = new selector.Selector();
935 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');
936 fields.forEach( function(field) {
937 query.addField(field, null, ['grouped']);
938 });
939 general_params.addMemberOptions(req, query, params);
940 general_params.addInitiativeOptions(req, query, params);
941 query.addOrderBy('event.id');
942 general_params.addLimitAndOffset(query, params);
943 db.query(conn, req, res, query, function (events, conn) {
944 var result = { result: events.rows }
945 includes = [];
946 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
947 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
948 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
949 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
950 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
951 addRelatedData(conn, req, res, result, includes);
952 });
953 }); },
955 // TODO add interfaces for data structure:
956 // ignored_member requireAccessLevel(conn, req, res, 'member');
957 // ignored_initiative requireAccessLevel(conn, req, res, 'member');
958 // setting requireAccessLevel(conn, req, res, 'member');
960 };
962 // ==========================================================================
963 // POST methods
964 // ==========================================================================
968 exports.post = {
970 '/echo_test': function (conn, req, res, params) { requireAccessLevel(conn, req, res, 'anonymous', function() {
971 respond('json', conn, req, res, 'ok', { result: params });
972 }); },
974 '/register_test': function (conn, req, res, params) {
975 var understood = params.understood;
976 var member_login = randomString(16);
977 var member_name = params.name;
978 var member_password = randomString(16);
979 var member_notify_email = params.email;
980 var member_notify_email_secret = randomString(24);
981 var api_key_member = randomString(24);
982 var api_key_full = randomString(24);
983 var api_key_pseudonym = randomString(24);
984 var api_key_anonymous = randomString(24);
986 if (understood != 'understood') {
987 respond('html', conn, req, res, 'unprocessable', null, 'You didn\'t checked the checkbox! Please hit back in your browser and try again.');
988 return;
989 }
991 // add member
992 var query = new selector.SQLInsert('member');
993 query.addValues({
994 login: member_login,
995 password: member_password, // TODO hashing of password
996 notify_email_unconfirmed: member_notify_email,
997 notify_email_secret: member_notify_email_secret,
998 name: member_name
999 });
1000 query.addReturning('id');
1001 db.query(conn, req, res, query, function (result, conn) {
1002 var member_id = result.rows[0].id;
1004 // add privilege for root unit
1005 var query = new selector.SQLInsert('privilege');
1006 query.addValues({ unit_id: 1, member_id: member_id, voting_right: true });
1007 db.query(conn, req, res, query, function (result, conn) {
1009 var location = params.location;
1010 var unit_id;
1011 switch(location) {
1012 case 'earth':
1013 unit_id = 3;
1014 break;
1015 case 'moon':
1016 unit_id = 4;
1017 break;
1018 case 'mars':
1019 unit_id = 5;
1020 break;
1023 // add privilege for selected planet
1024 var query = new selector.SQLInsert('privilege');
1025 query.addValues({ unit_id: unit_id, member_id: member_id, voting_right: true });
1026 db.query(conn, req, res, query, function (result, conn) {
1028 // add application key
1029 var query = new selector.SQLInsert('member_application');
1030 query.addValues({
1031 member_id: member_id,
1032 name: 'member',
1033 comment: 'access_level member',
1034 access_level: 'member',
1035 key: api_key_member
1036 });
1037 query.addReturning('id');
1039 db.query(conn, req, res, query, function (result, conn) {
1041 nodemailer.sendmail = '/usr/sbin/sendmail';
1043 // send email to user
1044 nodemailer.send_mail({
1045 sender: config.mail.from,
1046 subject: config.mail.subject_prefix + "Your LiquidFeedback API alpha test account needs confirmation",
1047 to: member_notify_email,
1048 body: "\
1049 Hello " + member_name + ",\n\
1050 \n\
1051 thank you for registering at the public alpha test of the LiquidFeedback\n\
1052 application programming interface. To complete the registration process,\n\
1053 you need to confirm your email address by opening the following URL:\n\
1054 \n\
1055 " + config.public_url_path + "register_test_confirm?secret=" + member_notify_email_secret + "\n\
1056 \n\
1057 \n\
1058 After you've confirmed your email address, your account will be automatically\n\
1059 activated.\n\
1060 \n\
1061 Your account name is: " + member_name + "\n\
1062 \n\
1063 \n\
1064 You will need the following login and password to register and unregister\n\
1065 applications for your account later. This function is currently not\n\
1066 implemented, but please keep the credentials for future use.\n\
1067 \n\
1068 Account ID: " + member_id + "\n\
1069 Login: " + member_login + "\n\
1070 Password: " + member_password + "\n\
1071 \n\
1072 \n\
1073 To make you able to actually access the API interface, we added the following\n\
1074 application key with full member access privileges to your account:\n\
1075 \n\
1076 API Key: " + api_key_member + "\n\
1077 \n\
1078 \n\
1079 The base address of the public test is: " + config.public_url_path + "\n\
1080 \n\
1081 The programming interface is described in the LiquidFeedback API\n\
1082 specification: http://dev.liquidfeedback.org/trac/lf/wiki/API\n\
1083 \n\
1084 The current implementation status of lfapi is published at the LiquidFeedback\n\
1085 API server page: http://dev.liquidfeedback.org/trac/lf/wiki/lfapi\n\
1086 \n\
1087 If you have any questions or suggestions, please use our public mailing list\n\
1088 at http://dev.liquidfeedback.org/cgi-bin/mailman/listinfo/main\n\
1089 \n\
1090 For issues regarding your test account, contact us via email at\n\
1091 lqfb-maintainers@public-software-group.org\n\
1092 \n\
1093 \n\
1094 Sincerely,\n\
1095 \n\
1096 Your LiquidFeedback maintainers",
1097 },
1098 function(err, result){
1099 if(err){ console.log(err); }
1100 });
1102 respond('html', conn, req, res, 'ok', 'Account created. Please check your mailbox!<br /><br /><br /><a href="/">Back to start page</a>');
1103 });
1104 });
1105 });
1106 });
1107 },
1109 /*
1110 '/register': function (conn, req, res, params) {
1111 var invite_key = params.invite_key;
1112 var login = params.login;
1113 var password = params.password;
1114 var name = params.name;
1115 var notify_email = params.notify_email;
1116 if (!invite_key) {
1117 respond('json', conn, req, res, 'unprocessable', null, 'No invite_key supplied.');
1118 return;
1119 };
1120 if (!login) {
1121 respond('json', conn, req, res, 'unprocessable', null, 'No login supplied.');
1122 return;
1123 };
1124 if (!password) {
1125 respond('json', conn, req, res, 'unprocessable', null, 'No password supplied.');
1126 return;
1127 };
1128 if (!name) {
1129 respond('json', conn, req, res, 'unprocessable', null, 'No name supplied.');
1130 return;
1131 };
1132 if (!notify_email) {
1133 respond('json', conn, req, res, 'unprocessable', null, 'No notify_email supplied.');
1134 return;
1135 };
1136 // check if akey is valid and get member_id for akey
1137 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) {
1138 if (result.rows.length != 1) {
1139 respond('json', conn, req, res, 'forbidden', null, 'Supplied invite_key is not valid.');
1140 return;
1141 };
1142 var member_id = result.rows[0].id;
1143 // check if name is available
1144 db.query(conn, req, res, { select: ['NULL'], from: ['member'], where: ['member.name = ' + db.pgEncode(name)] }, function (result, conn) {
1145 if (result.rows.length > 0) {
1146 respond('json', conn, req, res, 'forbidden', null, 'Login name is not available, choose another one.');
1147 return;
1148 };
1149 // check if login is available
1150 db.query(conn, req, res, { select: ['NULL'], from: ['member'], where: ['member.login = ' + db.pgEncode(login)] }, function (result, conn) {
1151 if (result.rows.length > 0) {
1152 respond('json', conn, req, res, 'forbidden', null, 'Name is not available, choose another one.');
1153 return;
1154 };
1155 var query = { update: 'member', set: { activation: 'now', active: true, } };
1157 });
1158 });
1159 });
1160 },
1161 */
1163 '/session': function (conn, req, res, params) {
1164 var key = params.key;
1165 if (!key) {
1166 respond('json', conn, req, res, 'unprocessable', null, 'No application key supplied.');
1167 return;
1168 };
1169 var query = new selector.Selector();
1170 query.from('member');
1171 query.join('member_application', null, 'member_application.member_id = member.id');
1172 query.addField('member.id');
1173 query.addWhere(['member.active AND member_application.key = ?', key]);
1174 if (params.interactive) {
1175 query.forUpdateOf('member');
1177 db.query(conn, req, res, query, function (result, conn) {
1178 if (result.rows.length != 1) {
1179 respond('json', conn, req, res, 'forbidden', null, 'Supplied application key is not valid.');
1180 return;
1181 };
1182 var member_id = result.rows[0].id;
1183 var session_key = randomString(16);
1184 req.sessions[session_key] = member_id;
1185 var query;
1186 if (params.interactive) {
1187 query = new selector.SQLUpdate('member');
1188 query.addWhere(['member.id = ?', member_id]);
1189 query.addValues({ last_activity: 'now' });
1191 db.query(conn, req, res, query, function (result, conn) {
1192 respond('json', conn, req, res, 'ok', { session_key: session_key });
1193 });
1194 });
1195 },
1197 '/member': function (conn, req, res, params) {
1198 var fields = ['organizational_unit', 'internal_posts', 'realname', 'birthday', 'address', 'email', 'xmpp_address', 'website', 'phone', 'mobile_phone', 'profession', 'external_memberships', 'external_posts', 'statement']
1199 requireAccessLevel(conn, req, res, 'member', function() {
1200 var query = new selector.SQLUpdate('member');
1201 query.addWhere(['member.id = ?', req.current_member_id]);
1202 fields.forEach( function(field) {
1203 if (typeof(params[field]) != 'undefined') {
1204 query.addValues({ field: params[field] });
1205 } else {
1206 query.addValues({ field: null });
1208 });
1209 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1210 });
1211 },
1213 '/membership': function (conn, req, res, params) {
1214 requireAccessLevel(conn, req, res, 'member', function() {
1216 // check if area_id is set
1217 var area_id = parseInt(params.area_id);
1218 if (!area_id) {
1219 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an area_id.');
1220 return;
1223 // delete membership
1224 if (params.delete) {
1225 var query;
1226 query = new selector.SQLDelete('membership');
1227 query.addWhere(['area_id = ?', area_id]);
1228 query.addWhere(['member_id = ?', req.current_member_id]);
1229 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1231 // add membership
1232 } else {
1234 // lock member for upsert
1235 lockMemberById(conn, req, res, req.current_member_id, function() {
1237 // check and lock privilege
1238 requireAreaPrivilege(conn, req, res, area_id, function() {
1240 // upsert membership
1241 var query = new selector.Upserter('membership', ['area_id', 'member_id']);
1242 query.addValues({ area_id: area_id, member_id: req.current_member_id });
1243 db.query(conn, req, res, query, function(result) {
1244 respond('json', conn, req, res, 'ok');
1245 });
1246 });
1247 });
1249 });
1250 },
1252 '/interest': function (conn, req, res, params) {
1253 requireAccessLevel(conn, req, res, 'member', function() {
1254 var query;
1256 // check if issue_id is set
1257 var issue_id = parseInt(params.issue_id);
1258 if (!issue_id) {
1259 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1260 return;
1263 // lock member for upsert
1264 lockMemberById(conn, req, res, req.current_member_id, function() {
1266 // delete interest
1267 if (params.delete) {
1269 // check issue state
1270 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1272 // delete interest
1273 query = new selector.SQLDelete('interest');
1274 query.addWhere(['issue_id = ?', issue_id]);
1275 query.addWhere(['member_id = ?', req.current_member_id]);
1276 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1277 });
1279 // add interest
1280 } else {
1282 // check and lock privilege
1283 requireIssuePrivilege(conn, req, res, issue_id, function() {
1285 // check issue state
1286 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1288 // upsert interest
1289 var query = new selector.Upserter('interest', ['issue_id', 'member_id']);
1290 query.addValues({ issue_id: issue_id, member_id: req.current_member_id });
1291 db.query(conn, req, res, query, function(result) {
1292 respond('json', conn, req, res, 'ok');
1293 });
1294 });
1295 });
1296 };
1297 });
1298 });
1299 },
1301 '/issue_comment': function (conn, req, res, params) {
1302 requireAccessLevel(conn, req, res, 'member', function() {
1304 var issue_id = parseInt(params.issue_id);
1305 var formatting_engine = params.formatting_engine
1306 var content = params.content;
1308 if (!issue_id) {
1309 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1310 return;
1313 // delete issue comment
1314 if (params.delete) {
1315 var query;
1316 query = new selector.SQLDelete('issue_comment');
1317 query.addWhere(['issue_id = ?', params.issue_id]);
1318 query.addWhere(['member_id = ?', req.current_member_id]);
1319 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1321 // upsert issue comment
1322 } else {
1324 // check if formatting engine is supplied and valid
1325 if (!formatting_engine) {
1326 respond('json', conn, req, res, 'unprocessable', null, 'No formatting engine supplied.');
1327 return;
1328 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1329 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1330 return;
1331 };
1333 // check if content is supplied
1334 if (!content) {
1335 respond('json', conn, req, res, 'unprocessable', null, 'No content supplied.');
1336 return;
1339 // lock member for upsert
1340 lockMemberById(conn, req, res, req.current_member_id, function() {
1342 // check and lock privilege
1343 requireIssuePrivilege(conn, req, res, issue_id, function() {
1345 // upsert issue comment
1346 var query = new selector.Upserter('issue_comment', ['issue_id', 'member_id']);
1347 query.addValues({
1348 issue_id: issue_id,
1349 member_id: req.current_member_id,
1350 changed: 'now',
1351 formatting_engine: formatting_engine,
1352 content: content
1353 });
1355 db.query(conn, req, res, query, function(result) {
1356 respond('json', conn, req, res, 'ok');
1357 });
1359 });
1360 });
1364 });
1365 },
1367 '/voting_comment': function (conn, req, res, params) {
1368 requireAccessLevel(conn, req, res, 'member', function() {
1370 var issue_id = parseInt(params.issue_id);
1371 var formatting_engine = params.formatting_engine
1372 var content = params.content;
1374 if (!issue_id) {
1375 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1376 return;
1380 // delete voting comment
1381 if (params.delete) {
1382 var query;
1383 query = new selector.SQLDelete('voting_comment');
1384 query.addWhere(['issue_id = ?', params.issue_id]);
1385 query.addWhere(['member_id = ?', req.current_member_id]);
1386 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1388 // upsert voting comment
1389 } else {
1391 // check if formatting engine is supplied and valid
1392 if (!formatting_engine) {
1393 respond('json', conn, req, res, 'unprocessable', null, 'No formatting engine supplied.');
1394 return;
1395 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1396 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1397 return;
1398 };
1400 // check if content is supplied
1401 if (!content) {
1402 respond('json', conn, req, res, 'unprocessable', null, 'No content supplied.');
1403 return;
1406 // lock member for upsert
1407 lockMemberById(conn, req, res, req.current_member_id, function() {
1409 // check and lock privilege
1410 requireIssuePrivilege(conn, req, res, issue_id, function() {
1412 // check issue state
1413 requireIssueState(conn, req, res, issue_id, ['voting', 'finished_with_winner', 'finished_without_winner'], function() {
1415 // upsert voting comment
1416 var query = new selector.Upserter('voting_comment', ['issue_id', 'member_id']);
1417 query.addValues({
1418 issue_id: issue_id,
1419 member_id: req.current_member_id,
1420 changed: 'now',
1421 formatting_engine: formatting_engine,
1422 content: content
1423 });
1425 db.query(conn, req, res, query, function(result) {
1426 respond('json', conn, req, res, 'ok');
1427 });
1429 });
1430 });
1431 })
1432 };
1433 });
1434 },
1436 '/supporter': function (conn, req, res, params) {
1437 requireAccessLevel(conn, req, res, 'member', function() {
1438 var initiative_id = parseInt(params.initiative_id);
1439 var draft_id = parseInt(params.draft_id);
1441 // check if needed arguments are supplied
1442 if (!initiative_id) {
1443 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an initiative_id.');
1444 return;
1447 if (!draft_id) {
1448 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an draft_id.');
1449 return;
1452 // lock member for upsert
1453 lockMemberById(conn, req, res, req.current_member_id, function() {
1455 // delete supporter
1456 if (params.delete) {
1458 // check issue state
1459 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion', 'verification'], function() {
1461 // delete supporter
1462 var query = new selector.SQLDelete('supporter');
1463 query.addWhere(['initiative_id = ?', initiative_id]);
1464 query.addWhere(['member_id = ?', req.current_member_id]);
1465 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1467 });
1469 // upsert supporter
1470 } else {
1472 // check and lock privilege
1473 requireInitiativePrivilege(conn, req, res, initiative_id, function() {
1475 // check issue state
1476 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion', 'verification'], function() {
1478 // check if given draft is the current one
1479 var query = new selector.Selector('current_draft');
1480 query.addField('NULL');
1481 query.addWhere(['current_draft.initiative_id = ?', initiative_id]);
1482 query.addWhere(['current_draft.id = ?', draft_id]);
1484 db.query(conn, req, res, query, function(result) {
1485 if (result.rows.length != 1) {
1486 respond('json', conn, req, res, 'conflict', null, 'The draft with the supplied draft_id is not the current one anymore!');
1487 return;
1490 // upsert supporter
1491 var query = new selector.Upserter('supporter', ['initiative_id', 'member_id']);
1492 query.addValues({
1493 initiative_id: initiative_id,
1494 member_id: req.current_member_id,
1495 draft_id: draft_id
1496 });
1498 db.query(conn, req, res, query, function(result) {
1499 respond('json', conn, req, res, 'ok');
1500 });
1502 });
1503 });
1504 });
1505 };
1506 });
1507 });
1508 },
1510 '/draft': function (conn, req, res, params) {
1511 requireAccessLevel(conn, req, res, 'member', function() {
1512 var area_id = parseInt(params.area_id);
1513 var policy_id = parseInt(params.policy_id);
1514 var issue_id = parseInt(params.issue_id);
1515 var initiative_id = parseInt(params.initiative_id);
1516 var initiative_name = params.initiative_name;
1517 var initiative_discussion_url = params.initiative_discussion_url;
1518 var formatting_engine = params.formatting_engine;
1519 var content = params.content;
1521 if (!initiative_discussion_url) initiative_discussion_url = null;
1523 // check parameters
1524 if (!formatting_engine) {
1525 respond('json', conn, req, res, 'unprocessable', null, 'No formatting_engine supplied.');
1526 return;
1527 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1528 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1529 return;
1530 };
1532 if (!content) {
1533 respond('json', conn, req, res, 'unprocessable', null, 'No draft content supplied.');
1534 return;
1535 };
1537 lockMemberById(conn, req, res, req.current_member_id, function() {
1539 // new draft in new initiative in new issue
1540 if (area_id && !issue_id && !initiative_id) {
1542 // check parameters for new issue
1543 if (!policy_id) {
1544 respond('json', conn, req, res, 'unprocessable', null, 'No policy supplied.');
1545 return;
1548 if (!initiative_name) {
1549 respond('json', conn, req, res, 'unprocessable', null, 'No initiative name supplied.');
1550 return;
1553 requireAreaPrivilege(conn, req, res, area_id, function() {
1555 // check if policy is allowed in this area and if area and policy are active
1556 var query = new selector.Selector();
1557 query.from('allowed_policy');
1558 query.join('area', null, 'area.id = allowed_policy.area_id AND area.active');
1559 query.join('policy', null, 'policy.id = allowed_policy.policy_id AND policy.active');
1560 query.addField('NULL');
1561 query.addWhere(['area.id = ? AND policy.id = ?', area_id, policy_id]);
1562 db.query(conn, req, res, query, function (result, conn) {
1563 if (result.rows.length != 1) {
1564 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.');
1565 return;
1566 };
1568 // check contingent
1569 requireContingentLeft(conn, req, res, true, function() {
1571 // insert new issue
1572 var query = new selector.SQLInsert('issue');
1573 query.addValues({
1574 area_id: area_id,
1575 policy_id: policy_id
1576 });
1577 query.addReturning('id');
1578 db.query(conn, req, res, query, function(result) {
1579 var issue_id = result.rows[0].id;
1581 // insert new initiative
1582 var query = new selector.SQLInsert('initiative');
1583 query.addValues({
1584 issue_id: issue_id,
1585 name: initiative_name,
1586 discussion_url: initiative_discussion_url
1587 });
1588 query.addReturning('id');
1589 db.query(conn, req, res, query, function(result) {
1590 var initiative_id = result.rows[0].id;
1592 // insert initiator
1593 var query = new selector.SQLInsert('initiator');
1594 query.addValues({ initiative_id: initiative_id, member_id: req.current_member_id, accepted: true });
1595 db.query(conn, req, res, query, function(result) {
1597 // insert new draft
1598 var query = new selector.SQLInsert('draft');
1599 query.addValues({
1600 initiative_id: initiative_id,
1601 author_id: req.current_member_id,
1602 formatting_engine: formatting_engine,
1603 content: content
1604 });
1605 query.addReturning('id');
1606 db.query(conn, req, res, query, function (result, conn) {
1607 var draft_id = result.rows[0].id;
1609 respond('json', conn, req, res, 'ok', { issue_id: issue_id, initiative_id: initiative_id, draft_id: draft_id } );
1610 });
1611 });
1612 });
1613 });
1614 });
1615 });
1616 });
1618 // new draft in new initiative in existant issue
1619 } else if (issue_id && !area_id && !initiative_id) {
1621 if (!initiative_name) {
1622 respond('json', conn, req, res, 'unprocessable', null, 'No initiative name supplied.');
1623 return;
1626 // check privilege
1627 requireIssuePrivilege(conn, req, res, issue_id, function() {
1629 // check issue state
1630 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1632 // check contingent
1633 requireContingentLeft(conn, req, res, true, function() {
1635 // insert initiative
1636 var query = new selector.SQLInsert('initiative');
1637 query.addValues({
1638 issue_id: issue_id,
1639 name: initiative_name,
1640 discussion_url: initiative_discussion_url
1641 });
1642 query.addReturning('id');
1643 db.query(conn, req, res, query, function(result) {
1644 var initiative_id = result.rows[0].id;
1646 // insert initiator
1647 var query = new selector.SQLInsert('initiator');
1648 query.addValues({
1649 initiative_id: initiative_id,
1650 member_id: req.current_member_id,
1651 accepted: true
1652 });
1653 db.query(conn, req, res, query, function(result) {
1655 // insert draft
1656 var query = new selector.SQLInsert('draft');
1657 query.addValues({
1658 initiative_id: initiative_id,
1659 author_id: req.current_member_id,
1660 formatting_engine: formatting_engine,
1661 content: content
1662 });
1663 query.addReturning('id');
1664 db.query(conn, req, res, query, function (result, conn) {
1666 var draft_id = result.rows[0].id;
1667 respond('json', conn, req, res, 'ok', { initiative_id: initiative_id, draft_id: draft_id } );
1669 });
1670 });
1671 });
1672 });
1673 });
1674 });
1676 // new draft in existant initiative
1677 } else if (initiative_id && !area_id && !issue_id ) {
1679 // check privilege
1680 requireInitiativePrivilege(conn, req, res, initiative_id, function() {
1682 // check issue state
1683 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion'], function() {
1686 // get initiator
1687 var query = new selector.Selector();
1688 query.from('initiator');
1689 query.addField('accepted');
1690 query.addWhere(['initiative_id = ? AND member_id = ?', initiative_id, req.current_member_id]);
1691 db.query(conn, req, res, query, function (result, conn) {
1693 // if member is not initiator, deny creating new draft
1694 if (result.rows.length != 1) {
1695 respond('json', conn, req, res, 'forbidden', null, 'You are not initiator of this initiative and not allowed to update its draft.');
1696 return;
1698 var initiator = result.rows[0];
1699 if (!initiator.accepted) {
1700 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.');
1701 return;
1702 };
1704 // check contingent
1705 requireContingentLeft(conn, req, res, false, function() {
1707 // insert new draft
1708 var query = new selector.SQLInsert('draft');
1709 query.addValues({
1710 initiative_id: initiative_id,
1711 author_id: req.current_member_id,
1712 formatting_engine: formatting_engine,
1713 content: content
1714 });
1715 query.addReturning('id');
1716 db.query(conn, req, res, query, function (result, conn) {
1718 var draft_id = result.rows[0].id;
1719 respond('json', conn, req, res, 'ok', { draft_id: draft_id } );
1720 });
1721 });
1722 });
1723 });
1724 });
1726 // none of them (invalid request)
1727 } else {
1728 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of area_id, issue_id or initiative_id must be supplied!');
1729 };
1731 });
1732 });
1733 },
1735 '/suggestion': function (conn, req, res, params) {
1736 requireAccessLevel(conn, req, res, 'member', function() {
1737 // TODO
1738 });
1739 },
1741 '/opinion': function (conn, req, res, params) {
1742 requireAccessLevel(conn, req, res, 'member', function() {
1743 // TODO
1744 });
1745 },
1747 '/delegation': function (conn, req, res, params) {
1748 requireAccessLevel(conn, req, res, 'member', function() {
1749 var unit_id = parseInt(params.unit_id);
1750 var area_id = parseInt(params.area_id);
1751 var issue_id = parseInt(params.issue_id);
1752 var trustee_id;
1754 if (params.trustee_id == '') {
1755 trustee_id = null;
1756 } else {
1757 trustee_id = parseInt(params.trustee_id);
1760 lockMemberById(conn, req, res, req.current_member_id, function() {
1762 if (params.delete) {
1763 var query = new selector.SQLDelete('delegation')
1764 if (unit_id && !area_id && !issue_id) {
1765 query.addWhere(['unit_id = ?', unit_id]);
1766 } else if (!unit_id && area_id && !issue_id) {
1767 query.addWhere(['area_id = ?', area_id]);
1768 } else if (!unit_id && !area_id && issue_id) {
1769 query.addWhere(['issue_id = ?', issue_id]);
1770 } else {
1771 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of unit, area_id, issue_id must be supplied!');
1772 return;
1774 query.addWhere(['truster_id = ?', req.current_member_id]);
1775 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1776 } else {
1777 var query = new selector.Upserter('delegation', ['truster_id']);
1778 query.addValues({
1779 truster_id: req.current_member_id,
1780 trustee_id: trustee_id
1781 });
1782 if (unit_id && !area_id && !issue_id) {
1784 // check privilege
1785 requireUnitPrivilege(conn, req, res, unit_id, function() {
1787 query.addKeys(['unit_id'])
1788 query.addValues({ unit_id: unit_id, scope: 'unit' });
1789 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1790 });
1792 } else if (!unit_id && area_id && !issue_id) {
1794 // check privilege
1795 requireAreaPrivilege(conn, req, res, area_id, function() {
1797 query.addKeys(['area_id'])
1798 query.addValues({ area_id: area_id, scope: 'area' });
1799 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1800 });
1802 } else if (!unit_id && !area_id && issue_id) {
1804 // check privilege
1805 requireIssuePrivilege(conn, req, res, issue_id, function() {
1807 // check issue state
1808 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification', 'voting'], function() {
1810 query.addKeys(['issue_id'])
1811 query.addValues({ issue_id: issue_id, scope: 'issue' });
1812 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1813 });
1814 });
1815 } else {
1816 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of unit_id, area_id, issue_id must be supplied!');
1817 return;
1821 });
1823 });
1824 },
1826 };

Impressum / About Us