lfapi

view lfapi/main.js @ 3:e69609a3c98a

Added member_ttl, added 'latest' snapshot to interest.
author bsw
date Thu Oct 20 13:55:29 2011 +0200 (2011-10-20)
parents 9fe872cc376d
children 77b761569ae2
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 email = require('mailer');
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 (conn && typeof(conn) != 'string') conn.drain();
83 if (mode == 'json') {
84 if (! object) object = {};
85 } else if (mode == 'html') {
86 if (! object) object = 'no content';
87 if (err) object = "Error: " + err;
88 }
90 object.status = status;
91 object.error = err;
93 if (mode == 'json') {
94 var body = JSON.stringify(object);
95 var content_type = 'application/json';
96 if (req.params && req.params.callback) {
97 body = req.params.callback + '(' + body + ')';
98 content_type = 'text/javascript';
99 }
100 res.writeHead(
101 http_status,
102 {
103 'Content-Type': content_type,
104 //'Content-Length': body.length
105 }
106 );
107 res.end(body);
108 } else if (mode == 'html') {
109 var body = ['<html><head><title>lfapi</title><style>body { font-family: sans-serif; }</style></head><body>']
110 body.push(object)
111 body.push('</body></html>')
112 body = body.join('');
113 res.writeHead(
114 http_status,
115 {
116 'Content-Type': 'text/html',
117 'Content-Length': body.length
118 }
119 );
120 res.end(body);
121 }
122 })
123 });
124 };
126 exports.respond = respond;
127 db.error_handler = respond;
129 // add requested related data for requests with include_* parameters
130 function addRelatedData(conn, req, res, result, includes) {
131 if (includes.length > 0) {
132 var include = includes.shift();
133 var class = include.class;
134 var objects = result[include.objects];
136 var query;
138 if (objects) {
139 var objects_exists = false;
140 var ids_hash = {};
141 if (typeof(objects) == 'array') {
142 if (objects.length > 0) {
143 objects_exists = true;
144 objects.forEach( function(object) {
145 if (object[class + "_id"]) {
146 ids_hash[object[class + "_id"]] = true;
147 };
148 });
149 }
150 } else {
151 for (var key in objects) {
152 objects_exists = true;
153 var object = objects[key];
154 if (object[class + "_id"]) {
155 ids_hash[object[class + "_id"]] = true;
156 };
157 };
158 };
160 if (objects_exists) {
161 var ids = [];
162 for (key in ids_hash) {
163 ids.push(key)
164 }
165 if (ids.length > 0) {
166 query = new selector.Selector();
167 query.from(class);
168 query.addWhere([class + '.id IN (??)', ids]);
169 fields.addObjectFields(query, class);
170 }
171 };
172 };
174 db.query(conn, req, res, query, function (result2, conn) {
175 // add result to main result, regarding correct pluralization
176 var tmp = {};
177 if (result2) {
178 result2.rows.forEach( function(row) {
179 tmp[row.id] = row;
180 });
181 };
183 if (class == 'policy') {
184 result['policies'] = tmp;
185 } else {
186 result[class + 's'] = tmp;
187 }
188 addRelatedData(conn, req, res, result, includes);
189 });
190 } else {
191 respond('json', conn, req, res, 'ok', result);
192 };
194 };
196 function lockMemberById(conn, req, res, member_id, callback) {
197 var query = new selector.Selector('member');
198 query.addField('NULL');
199 query.addWhere(['member.id = ?', member_id]);
200 query.forUpdate();
201 db.query(conn, req, res, query, callback);
202 };
204 function requireUnitPrivilege(conn, req, res, unit_id, callback) {
205 var query = new selector.Selector('privilege');
206 query.addField('NULL');
207 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
208 query.addWhere(['privilege.unit_id = ?', unit_id ]);
209 query.addWhere('privilege.voting_right');
210 query.forShareOf('privilege');
211 db.query(conn, req, res, query, function(result, conn) {
212 if (result.rows.length != 1) {
213 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for this unit.');
214 return;
215 }
216 callback();
217 });
218 };
220 function requireAreaPrivilege(conn, req, res, area_id, callback) {
221 var query = new selector.Selector('privilege');
222 query.join('area', null, 'area.unit_id = privilege.unit_id');
223 query.addField('NULL');
224 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
225 query.addWhere(['area.id = ?', area_id ]);
226 query.addWhere('privilege.voting_right');
227 query.forShareOf('privilege');
228 db.query(conn, req, res, query, function(result, conn) {
229 if (result.rows.length != 1) {
230 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for areas in this unit.');
231 return;
232 }
233 callback();
234 });
235 };
237 function requireIssuePrivilege(conn, req, res, issue_id, callback) {
238 var query = new selector.Selector('privilege');
239 query.join('area', null, 'area.unit_id = privilege.unit_id');
240 query.join('issue', null, 'issue.area_id = area.id');
241 query.addField('NULL');
242 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
243 query.addWhere(['issue.id = ?', issue_id ]);
244 query.addWhere('privilege.voting_right');
245 query.forShareOf('privilege');
246 db.query(conn, req, res, query, function(result, conn) {
247 if (result.rows.length != 1) {
248 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for issues in this unit.');
249 return;
250 }
251 callback();
252 });
253 };
255 function requireInitiativePrivilege(conn, req, res, initiative_id, callback) {
256 var query = new selector.Selector('privilege');
257 query.join('area', null, 'area.unit_id = privilege.unit_id');
258 query.join('issue', null, 'issue.area_id = area.id');
259 query.join('initiative', null, 'initiative.issue_id = issue.id');
260 query.addField('NULL');
261 query.addWhere(['privilege.member_id = ?', req.current_member_id]);
262 query.addWhere(['initiative.id = ?', initiative_id ]);
263 query.addWhere('privilege.voting_right');
264 query.forShareOf('privilege');
265 db.query(conn, req, res, query, function(result, conn) {
266 if (result.rows.length != 1) {
267 respond('json', conn, req, res, 'forbidden', null, 'You have no voting right for initiatives in this unit.');
268 return;
269 }
270 callback();
271 });
272 };
274 function requireIssueState(conn, req, res, issue_id, required_states, callback) {
275 var query = new selector.Selector('issue');
276 query.addField('NULL');
277 query.addWhere(['issue.id = ?', issue_id]);
278 query.addWhere(['issue.state IN (??)', required_states]);
279 query.forUpdateOf('issue');
280 db.query(conn, req, res, query, function(result, conn) {
281 if (result.rows.length != 1) {
282 respond('json', conn, req, res, 'forbidden', null, 'Issue is in wrong state.');
283 return;
284 }
285 callback();
286 });
287 };
289 function requireIssueStateForInitiative(conn, req, res, initiative_id, required_states, callback) {
290 var query = new selector.Selector('issue');
291 query.join('initiative', null, 'initiative.issue_id = issue.id');
292 query.addField('NULL');
293 query.addWhere(['initiative.id = ?', initiative_id]);
294 query.addWhere(['issue.state IN (??)', required_states]);
295 query.forUpdateOf('issue');
296 db.query(conn, req, res, query, function(result, conn) {
297 if (result.rows.length != 1) {
298 respond('json', conn, req, res, 'forbidden', null, 'Issue is in wrong state.');
299 return;
300 }
301 callback();
302 });
303 }
305 function requireContingentLeft(conn, req, res, is_initiative, callback) {
306 var query = new selector.Selector('member_contingent_left');
307 query.addField('NULL');
308 query.addWhere(['member_contingent_left.member_id = ?', req.current_member_id]);
309 query.addWhere('member_contingent_left.text_entries_left >= 1');
310 if (is_initiative) {
311 query.addWhere('member_contingent_left.initiatives_left >= 1');
312 }
313 db.query(conn, req, res, query, function(result, conn) {
314 if (result.rows.length != 1) {
315 respond('json', conn, req, res, 'forbidden', null, 'Contingent empty.');
316 return;
317 }
318 callback();
319 });
320 }
322 // ==========================================================================
323 // GETT methods
324 // ==========================================================================
327 exports.get = {
329 // startpage (html) for users
330 // currently used for implementing public alpha test
331 '/': function (conn, req, res, params) {
333 var html = [];
334 html.push('<h2>welcome to lfapi public developer alpha test</h2>');
335 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>');
336 html.push('<h2>how to use</h2>');
337 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>')
338 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>');
339 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>');
340 html.push('<h2>questions and suggestions</h2>');
341 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>');
342 html.push('<h2>developer registration</h2>');
343 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 />');
344 html.push('<form action="register_test" method="POST">');
345 html.push('<label for="name">Your name:</label> <input type="text" id="name" name="name" /> &nbsp; &nbsp; ');
346 html.push('<label for="email">Email address:</label> <input type="text" id="email" name="email" /> &nbsp; &nbsp; ');
347 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>');
348 html.push('<br />');
349 html.push('<br />');
350 html.push('<div style="border: 2px solid #c00000; background-color: #ffa0a0; padding: 1ex;">');
351 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>!');
352 html.push('<br />');
353 html.push('<br />');
354 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 />');
355 html.push('</div>');
356 html.push('<br />');
357 html.push('<input type="submit" value="Register account" />');
358 respond('html', null, req, res, 'ok', html.join(''));
359 },
361 // temporary method to implement public alpha test
362 '/register_test_confirm': function (conn, req, res, params) {
363 var secret = params.secret;
365 var query = new selector.Selector('member');
366 query.addField('member.id, member.notify_email_unconfirmed');
367 query.addWhere(['member.notify_email_secret = ?', secret]);
368 db.query(conn, req, res, query, function (result, conn) {
369 var member = result.rows[0];
370 if (member) {
371 var query = new selector.SQLUpdate('member');
372 query.addValues({
373 notify_email: member.notify_email_unconfirmed,
374 notify_email_secret: null,
375 notify_email_unconfirmed: null,
376 active: true,
377 activated: 'now',
378 active: true,
379 last_activity: 'now',
380 locked: false
381 });
382 query.addWhere(['id = ?', member.id]);
383 db.query(conn, req, res, query, function (err, result) {
384 respond('html', conn, req, res, 'ok', 'Account activated: ');
385 });
386 } else {
387 respond('html', conn, req, res, 'forbidden', 'Secret not valid or already used.');
388 }
389 })
390 },
392 '/info': function (conn, req, res, params) {
393 requireAccessLevel(conn, req, res, 'anonymous', function() {
394 var query = new selector.Selector();
395 query.from('"liquid_feedback_version"');
396 query.addField('"liquid_feedback_version".*');
397 db.query(conn, req, res, query, function (result, conn) {
398 var liquid_feedback_version = result.rows[0];
399 var query = new selector.Selector();
400 query.from('"system_setting"');
401 query.addField('"member_ttl"');
402 db.query(conn, req, res, query, function (result, conn) {
403 var member_ttl = null;
404 if (result.rows[0]) {
405 member_ttl = result.rows[0].member_ttl;
406 };
407 respond('json', conn, req, res, 'ok', {
408 core_version: liquid_feedback_version.string,
409 api_version: api_version,
410 current_access_level: req.current_member_id ? 'member' : req.current_access_level,
411 current_member_id: req.current_member_id,
412 member_ttl: member_ttl,
413 settings: config.settings
414 });
415 });
416 });
417 });
418 },
420 '/member_count': function (conn, req, res, params) {
421 requireAccessLevel(conn, req, res, 'anonymous', function() {
422 var query = new selector.Selector();
423 query.from('"member_count"');
424 query.addField('"member_count".*');
425 db.query(conn, req, res, query, function (result, conn) {
426 var member_count = result.rows[0];
427 respond('json', conn, req, res, 'ok', {
428 total_count: member_count.total_count,
429 calculated: member_count.calculated
430 });
431 });
432 });
433 },
435 '/contingent': function (conn, req, res, params) {
436 requireAccessLevel(conn, req, res, 'anonymous', function() {
437 var query = new selector.Selector();
438 query.from('"contingent"');
439 query.addField('"contingent".*');
440 db.query(conn, req, res, query, function (result, conn) {
441 respond('json', conn, req, res, 'ok', { result: result.rows });
442 });
443 });
444 },
446 '/contingent_left': function (conn, req, res, params) {
447 requireAccessLevel(conn, req, res, 'member', function() {
448 var query = new selector.Selector();
449 query.from('"member_contingent_left"');
450 query.addField('"member_contingent_left".text_entries_left');
451 query.addField('"member_contingent_left".initiatives_left');
452 query.addWhere(['member_id = ?', req.current_member_id]);
453 db.query(conn, req, res, query, function (result, conn) {
454 respond('json', conn, req, res, 'ok', { result: result.rows[0] });
455 });
456 });
457 },
459 '/member': function (conn, req, res, params) {
460 requireAccessLevel(conn, req, res, 'pseudonym', function() {
461 var query = new selector.Selector();
462 query.from('"member"');
463 if (req.current_access_level == 'pseudonym' && !req.current_member_id ) {
464 fields.addObjectFields(query, 'member', 'member_pseudonym');
465 } else {
466 fields.addObjectFields(query, 'member');
467 }
468 general_params.addMemberOptions(req, query, params);
469 query.addOrderBy('"member"."id"');
470 general_params.addLimitAndOffset(query, params);
471 db.query(conn, req, res, query, function (result, conn) {
472 respond('json', conn, req, res, 'ok', { result: result.rows });
473 });
474 });
475 },
477 '/member_history': function (conn, req, res, params) {
478 requireAccessLevel(conn, req, res, 'full', function() {
479 var query = new selector.Selector();
480 query.from('"member_history" JOIN "member" ON "member"."id" = "member_history"."member_id"');
481 query.addField('"member_history".*');
482 general_params.addMemberOptions(req, query, params);
483 query.addOrderBy('member_history.id');
484 general_params.addLimitAndOffset(query, params);
485 db.query(conn, req, res, query, function (member_history_result, conn) {
486 var result = { result: member_history_result.rows }
487 includes = [];
488 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
489 addRelatedData(conn, req, res, result, includes);
490 });
491 });
492 },
494 '/member_image': function (conn, req, res, params) {
495 requireAccessLevel(conn, req, res, 'full', function() {
496 var query = new selector.Selector();
497 query.from('"member_image" JOIN "member" ON "member"."id" = "member_image"."member_id"');
498 query.addField('"member_image".*');
499 query.addWhere('member_image.scaled');
500 general_params.addMemberOptions(req, query, params);
501 query.addOrderBy = ['member_image.member_id, member_image.image_type'];
502 db.query(conn, req, res, query, function (result, conn) {
503 respond('json', conn, req, res, 'ok', { result: result.rows });
504 });
505 });
506 },
508 '/contact': function (conn, req, res, params) {
509 requireAccessLevel(conn, req, res, 'pseudonym', function() {
510 var query = new selector.Selector();
511 query.from('contact JOIN member ON member.id = contact.member_id');
512 query.addField('"contact".*');
513 if (req.current_member_id) {
514 // public or own for members
515 query.addWhere(['"contact"."public" OR "contact"."member_id" = ?', req.current_member_id]);
516 } else {
517 // public for everybody
518 query.addWhere('"contact"."public"');
519 }
520 general_params.addMemberOptions(req, query, params);
521 query.addOrderBy('"contact"."id"');
522 general_params.addLimitAndOffset(query, params);
523 db.query(conn, req, res, query, function (result, conn) {
524 respond('json', conn, req, res, 'ok', { result: result.rows });
525 });
526 });
527 },
529 '/privilege': function (conn, req, res, params) {
530 requireAccessLevel(conn, req, res, 'pseudonym', function() {
531 var query = new selector.Selector();
532 query.from('privilege JOIN member ON member.id = privilege.member_id JOIN unit ON unit.id = privilege.unit_id');
533 query.addField('privilege.*');
534 general_params.addUnitOptions(req, query, params);
535 general_params.addMemberOptions(req, query, params);
536 query.addOrderBy('privilege.unit_id, privilege.member_id');
537 general_params.addLimitAndOffset(query, params);
538 db.query(conn, req, res, query, function (privilege_result, conn) {
539 var result = { result: privilege_result.rows }
540 includes = [];
541 if (params.include_units) includes.push({ class: 'unit', objects: 'result'});
542 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
543 addRelatedData(conn, req, res, result, includes);
544 });
545 });
546 },
548 '/policy': function (conn, req, res, params) {
549 requireAccessLevel(conn, req, res, 'anonymous', function() {
550 var query = new selector.Selector();
551 query.from('"policy"');
552 query.addField('"policy".*');
553 general_params.addPolicyOptions(req, query, params);
554 query.addOrderBy('"policy"."index"');
555 general_params.addLimitAndOffset(query, params);
556 db.query(conn, req, res, query, function (result, conn) {
557 respond('json', conn, req, res, 'ok', { result: result.rows });
558 });
559 });
560 },
562 '/unit': function (conn, req, res, params) {
563 requireAccessLevel(conn, req, res, 'anonymous', function() {
564 var query = new selector.Selector();
565 query.from('"unit"');
566 fields.addObjectFields(query, 'unit');
567 general_params.addUnitOptions(req, query, params);
568 query.addOrderBy('unit.id');
569 general_params.addLimitAndOffset(query, params);
570 db.query(conn, req, res, query, function (result, conn) {
571 respond('json', conn, req, res, 'ok', { result: result.rows });
572 });
573 });
574 },
576 '/area': function (conn, req, res, params) {
577 requireAccessLevel(conn, req, res, 'anonymous', function() {
578 var query = new selector.Selector();
579 query.from('area JOIN unit ON area.unit_id = unit.id');
580 fields.addObjectFields(query, 'area');
581 general_params.addAreaOptions(req, query, params);
582 query.addOrderBy('area.id');
583 general_params.addLimitAndOffset(query, params);
584 db.query(conn, req, res, query, function (area_result, conn) {
585 var result = { result: area_result.rows }
586 includes = [];
587 if (params.include_units) includes.push({ class: 'unit', objects: 'result'});
588 addRelatedData(conn, req, res, result, includes);
589 });
590 });
591 },
593 '/allowed_policy': function (conn, req, res, params) {
594 requireAccessLevel(conn, req, res, 'anonymous', function() {
595 var query = new selector.Selector();
596 query.from('allowed_policy');
597 query.join('area', null, 'area.id = allowed_policy.area_id');
598 query.join('unit', null, 'unit.id = area.unit_id');
599 query.addField('allowed_policy.*');
600 general_params.addAreaOptions(req, query, params);
601 query.addOrderBy('allowed_policy.area_id, allowed_policy.policy_id');
602 general_params.addLimitAndOffset(query, params);
603 db.query(conn, req, res, query, function (allowed_policy_result, conn) {
604 var result = { result: allowed_policy_result.rows }
605 includes = [];
606 if (params.include_policies) includes.push({ class: 'policy', objects: 'result'});
607 if (params.include_areas) includes.push({ class: 'area', objects: 'result'});
608 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
609 addRelatedData(conn, req, res, result, includes);
610 });
611 }); },
613 '/membership': function (conn, req, res, params) {
614 requireAccessLevel(conn, req, res, 'pseudonym', function() {
615 var query = new selector.Selector();
616 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');
617 query.addField('membership.*');
618 general_params.addAreaOptions(req, query, params);
619 general_params.addMemberOptions(req, query, params);
620 query.addOrderBy('membership.area_id, membership.member_id');
621 general_params.addLimitAndOffset(query, params);
622 db.query(conn, req, res, query, function (membership_result, conn) {
623 var result = { result: membership_result.rows }
624 includes = [];
625 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
626 if (params.include_areas) includes.push({ class: 'area', objects: 'result'});
627 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
628 addRelatedData(conn, req, res, result, includes);
629 });
630 });
631 },
633 '/issue': function (conn, req, res, params) {
634 requireAccessLevel(conn, req, res, 'anonymous', function() {
635 var query = new selector.Selector()
636 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');
637 fields.addObjectFields(query, 'issue');
638 general_params.addIssueOptions(req, query, params);
639 query.addOrderBy('issue.id');
640 general_params.addLimitAndOffset(query, params);
641 db.query(conn, req, res, query, function (issue_result, conn) {
642 var result = { result: issue_result.rows }
643 includes = [];
644 if (params.include_areas) includes.push({ class: 'area', objects: 'result'});
645 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
646 if (params.include_policies) includes.push({ class: 'policy', objects: 'result' });
647 addRelatedData(conn, req, res, result, includes);
648 });
649 });
650 },
652 '/interest': function (conn, req, res, params) {
653 requireAccessLevel(conn, req, res, 'pseudonym', function() {
654 var query = new selector.Selector();
655 if (!params.snapshot) {
656 query.from('interest');
657 } else if (params.snapshot == 'latest') {
658 query.from('direct_interest_snapshot', 'interest');
659 query.addWhere('interest.event = issue.latest_snapshot_event');
660 };
661 query.addField('interest.*');
662 query.join('member', null, 'member.id = interest.member_id');
663 query.join('issue', null, 'interest.issue_id = issue.id');
664 query.join('policy', null, 'policy.id = issue.policy_id');
665 query.join('area', null, 'area.id = issue.area_id');
666 query.join('unit', null, 'area.unit_id = unit.id');
667 general_params.addMemberOptions(req, query, params);
668 general_params.addIssueOptions(req, query, params);
669 query.addOrderBy('interest.issue_id, interest.member_id');
670 general_params.addLimitAndOffset(query, params);
671 db.query(conn, req, res, query, function (interest_result, conn) {
672 var result = { result: interest_result.rows }
673 includes = [];
674 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
675 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
676 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
677 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
678 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
679 addRelatedData(conn, req, res, result, includes);
680 });
681 });
682 },
684 '/issue_comment': function (conn, req, res, params) {
685 requireAccessLevel(conn, req, res, 'pseudonym', function() {
686 var query = new selector.Selector();
687 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');
688 query.addField('issue_comment.*');
689 general_params.addMemberOptions(req, query, params);
690 general_params.addIssueOptions(req, query, params);
691 query.addOrderBy('issue_comment.issue_id, issue_comment.member_id');
692 general_params.addLimitAndOffset(query, params);
693 db.query(conn, req, res, query, function (issue_comment_result, conn) {
694 var result = { result: issue_comment_result.rows }
695 includes = [];
696 if (params.include_members) includes.push({ class: 'member', objects: 'result'});
697 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
698 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
699 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
700 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
701 addRelatedData(conn, req, res, result, includes);
702 });
703 });
704 },
706 '/initiative': function (conn, req, res, params) {
707 requireAccessLevel(conn, req, res, 'anonymous', function() {
708 var query = new selector.Selector();
709 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');
710 fields.addObjectFields(query, 'initiative');
711 query.addOrderBy('initiative.id');
712 general_params.addInitiativeOptions(req, query, params);
713 general_params.addLimitAndOffset(query, params);
714 db.query(conn, req, res, query, function (initiative_result, conn) {
715 var result = { result: initiative_result.rows }
716 includes = [];
717 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
718 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
719 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
720 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
721 addRelatedData(conn, req, res, result, includes);
722 });
723 });
724 },
726 '/initiator': function (conn, req, res, params) {
727 requireAccessLevel(conn, req, res, 'pseudonym', function() {
728 var fields = ['initiator.initiative_id', 'initiator.member_id'];
729 var query = new selector.Selector();
730 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');
731 query.addWhere('initiator.accepted');
732 fields.forEach( function(field) {
733 query.addField(field, null, ['grouped']);
734 });
735 general_params.addMemberOptions(req, query, params);
736 general_params.addInitiativeOptions(req, query, params);
737 query.addOrderBy('initiator.initiative_id, initiator.member_id');
738 general_params.addLimitAndOffset(query, params);
739 db.query(conn, req, res, query, function (initiator, conn) {
740 var result = { result: initiator.rows }
741 includes = [];
742 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
743 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
744 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
745 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
746 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
747 addRelatedData(conn, req, res, result, includes);
748 });
749 });
750 },
753 '/supporter': function (conn, req, res, params) {
754 requireAccessLevel(conn, req, res, 'pseudonym', function() {
755 var fields = ['supporter.issue_id', 'supporter.initiative_id', 'supporter.member_id', 'supporter.draft_id'];
756 var query = new selector.Selector();
757 query.from('supporter')
758 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');
759 fields.forEach( function(field) {
760 query.addField(field, null, ['grouped']);
761 });
762 general_params.addMemberOptions(req, query, params);
763 general_params.addInitiativeOptions(req, query, params);
764 query.addOrderBy('supporter.issue_id, supporter.initiative_id, supporter.member_id');
765 general_params.addLimitAndOffset(query, params);
766 db.query(conn, req, res, query, function (supporter, conn) {
767 var result = { result: supporter.rows }
768 includes = [];
769 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
770 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
771 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
772 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
773 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
774 addRelatedData(conn, req, res, result, includes);
775 });
776 });
777 },
779 '/battle': function (conn, req, res, params) {
780 requireAccessLevel(conn, req, res, 'anonymous', function() {
781 var query = new selector.Selector();
782 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');
783 query.addField('battle.*');
784 general_params.addInitiativeOptions(req, query, params);
785 query.addOrderBy('battle.issue_id, battle.winning_initiative_id, battle.losing_initiative_id');
786 general_params.addLimitAndOffset(query, params);
787 db.query(conn, req, res, query, function (result, conn) {
788 var result = { result: result.rows }
789 includes = [];
790 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
791 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
792 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
793 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
794 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
795 addRelatedData(conn, req, res, result, includes);
796 });
797 });
798 },
800 '/draft': function (conn, req, res, params) {
801 requireAccessLevel(conn, req, res, 'anonymous', function() {
802 var fields = ['draft.initiative_id', 'draft.id', 'draft.formatting_engine', 'draft.content', 'draft.author_id'];
803 var query = new selector.Selector();
804 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');
805 fields.forEach( function(field) {
806 query.addField(field, null, ['grouped']);
807 });
808 if (req.current_access_level != 'anonymous' || req.current_member_id) {
809 query.addField('draft.author_id');
810 }
811 if (params.draft_id) {
812 query.addWhere('draft.id = ?', params.draft_id);
813 }
814 if (params.current_draft) {
815 query.join('current_draft', null, 'current_draft.initiative_id = initiative.id AND current_draft.id = draft.id')
816 }
817 general_params.addInitiativeOptions(req, query, params);
818 query.addOrderBy('draft.initiative_id, draft.id');
819 general_params.addLimitAndOffset(query, params);
820 db.query(conn, req, res, query, function (result, conn) {
821 var result = { result: result.rows }
822 includes = [];
823 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
824 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
825 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
826 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
827 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
828 addRelatedData(conn, req, res, result, includes);
829 });
830 });
831 },
833 '/suggestion': function (conn, req, res, params) {
834 requireAccessLevel(conn, req, res, 'anonymous', function() {
835 var query = new selector.Selector();
836 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');
837 if (req.current_access_level == 'anonymous' && !req.current_member_id ) {
838 fields.addObjectFields(query, 'suggestion', 'suggestion_pseudonym');
839 } else {
840 fields.addObjectFields(query, 'suggestion');
841 }
842 general_params.addSuggestionOptions(req, query, params);
843 query.addOrderBy('suggestion.initiative_id, suggestion.id');
844 general_params.addLimitAndOffset(query, params);
845 db.query(conn, req, res, query, function (result, conn) {
846 var result = { result: result.rows }
847 includes = [];
848 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
849 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
850 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
851 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
852 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
853 addRelatedData(conn, req, res, result, includes);
854 });
855 });
856 },
858 '/opinion': function (conn, req, res, params) {
859 requireAccessLevel(conn, req, res, 'pseudonym', function() {
860 var fields = ['opinion.initiative_id', 'opinion.suggestion_id', 'opinion.member_id', 'opinion.degree', 'opinion.fulfilled']
861 var query = new selector.Selector();
862 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');
863 fields.forEach( function(field) {
864 query.addField(field, null, ['grouped']);
865 });
866 general_params.addMemberOptions(req, query, params);
867 general_params.addSuggestionOptions(req, query, params);
868 query.addOrderBy = ['opinion.initiative_id, opinion.suggestion_id, opinion.member_id'];
869 general_params.addLimitAndOffset(query, params);
870 db.query(conn, req, res, query, function (result, conn) {
871 var result = { result: result.rows }
872 includes = [];
873 if (params.include_suggestions) includes.push({ class: 'suggestion', objects: 'result'});
874 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'suggestions'});
875 if (params.include_issues) includes.push({ class: 'issue', objects: 'initiatives'});
876 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
877 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
878 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
879 addRelatedData(conn, req, res, result, includes);
880 });
881 });
882 },
884 '/delegation': function (conn, req, res, params) {
885 requireAccessLevel(conn, req, res, 'pseudonym', function() {
886 var fields = ['delegation.id', 'delegation.truster_id', 'delegation.trustee_id', 'delegation.scope', 'delegation.area_id', 'delegation.issue_id', 'delegation.unit_id'];
887 var query = new selector.Selector();
888 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');
889 fields.forEach( function(field) {
890 query.addField(field, null, ['grouped']);
891 });
892 if (params.direction) {
893 switch (params.direction) {
894 case 'in':
895 query.join('member', null, 'member.id = delegation.trustee_id');
896 break;
897 case 'out':
898 query.join('member', null, 'member.id = delegation.truster_id');
899 break;
900 default:
901 respond('json', conn, req, res, 'unprocessable', 'Direction must be "in" or "out" if set.');
902 }
903 } else {
904 query.join('member', null, 'member.id = delegation.truster_id OR member.id = delegation.trustee_id');
905 }
906 general_params.addMemberOptions(req, query, params);
907 general_params.addIssueOptions(req, query, params);
908 if (params.scope) {
909 query.addWhere(['delegation.scope IN (??)', params.scope.split(',')]);
910 };
911 query.addOrderBy = ['delegation.id'];
912 general_params.addLimitAndOffset(query, params);
913 db.query(conn, req, res, query, function (result, conn) {
914 respond('json', conn, req, res, 'ok', { result: result.rows });
915 });
916 });
917 },
919 '/vote': function (conn, req, res, params) {
920 requireAccessLevel(conn, req, res, 'pseudonym', function() {
921 var query = new selector.Selector();
922 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');
923 query.addField('vote.*');
924 query.addWhere('issue.closed_at NOTNULL');
925 general_params.addMemberOptions(req, query, params);
926 general_params.addInitiativeOptions(req, query, params);
927 general_params.addLimitAndOffset(query, params);
928 db.query(conn, req, res, query, function (result, conn) {
929 respond('json', conn, req, res, 'ok', { result: result.rows });
930 });
931 });
932 },
934 '/event': function (conn, req, res, params) { requireAccessLevel(conn, req, res, 'anonymous', function() {
935 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'];
936 var query = new selector.Selector();
937 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');
938 fields.forEach( function(field) {
939 query.addField(field, null, ['grouped']);
940 });
941 general_params.addMemberOptions(req, query, params);
942 general_params.addInitiativeOptions(req, query, params);
943 query.addOrderBy('event.id');
944 general_params.addLimitAndOffset(query, params);
945 db.query(conn, req, res, query, function (events, conn) {
946 var result = { result: events.rows }
947 includes = [];
948 if (params.include_initiatives) includes.push({ class: 'initiative', objects: 'result'});
949 if (params.include_issues) includes.push({ class: 'issue', objects: 'result'});
950 if (params.include_areas) includes.push({ class: 'area', objects: 'issues'});
951 if (params.include_units) includes.push({ class: 'unit', objects: 'areas'});
952 if (params.include_policies) includes.push({ class: 'policy', objects: 'issues' });
953 addRelatedData(conn, req, res, result, includes);
954 });
955 }); },
957 // TODO add interfaces for data structure:
958 // event requireAccessLevel(conn, req, res, 'member');
959 // ignored_member requireAccessLevel(conn, req, res, 'member');
960 // ignored_initiative requireAccessLevel(conn, req, res, 'member');
961 // setting requireAccessLevel(conn, req, res, 'member');
963 };
965 // ==========================================================================
966 // POST methods
967 // ==========================================================================
971 exports.post = {
973 '/echo_test': function (conn, req, res, params) { requireAccessLevel(conn, req, res, 'anonymous', function() {
974 respond('json', conn, req, res, 'ok', { result: params });
975 }); },
977 '/register_test': function (conn, req, res, params) {
978 var understood = params.understood;
979 var member_login = randomString(16);
980 var member_name = params.name;
981 var member_password = randomString(16);
982 var member_notify_email = params.email;
983 var member_notify_email_secret = randomString(24);
984 var api_key_member = randomString(24);
985 var api_key_full = randomString(24);
986 var api_key_pseudonym = randomString(24);
987 var api_key_anonymous = randomString(24);
989 if (understood != 'understood') {
990 respond('html', conn, req, res, 'unprocessable', null, 'You didn\'t checked the checkbox! Please hit back in your browser and try again.');
991 return;
992 }
994 // add member
995 var query = new selector.SQLInsert('member');
996 query.addValues({
997 login: member_login,
998 password: member_password, // TODO hashing of password
999 notify_email_unconfirmed: member_notify_email,
1000 notify_email_secret: member_notify_email_secret,
1001 name: member_name
1002 });
1003 query.addReturning('id');
1004 db.query(conn, req, res, query, function (result, conn) {
1005 var member_id = result.rows[0].id;
1007 // add privilege for root unit
1008 var query = new selector.SQLInsert('privilege');
1009 query.addValues({ unit_id: 1, member_id: member_id, voting_right: true });
1010 db.query(conn, req, res, query, function (result, conn) {
1012 var location = params.location;
1013 var unit_id;
1014 switch(location) {
1015 case 'earth':
1016 unit_id = 3;
1017 break;
1018 case 'moon':
1019 unit_id = 4;
1020 break;
1021 case 'mars':
1022 unit_id = 5;
1023 break;
1026 // add privilege for selected planet
1027 var query = new selector.SQLInsert('privilege');
1028 query.addValues({ unit_id: unit_id, member_id: member_id, voting_right: true });
1029 db.query(conn, req, res, query, function (result, conn) {
1031 // add application key
1032 var query = new selector.SQLInsert('member_application');
1033 query.addValues({
1034 member_id: member_id,
1035 name: 'member',
1036 comment: 'access_level member',
1037 access_level: 'member',
1038 key: api_key_member
1039 });
1040 query.addReturning('id');
1042 db.query(conn, req, res, query, function (result, conn) {
1044 // send email to user
1045 email.send({
1046 host : config.mail.smtp_host,
1047 port: config.mail.smtp_port,
1048 ssl: config.mail.smtp_ssl,
1049 domain: config.mail.smtp_domain,
1050 authentication: config.mail.smtp_authentication,
1051 username: config.mail.smtp_username,
1052 password: config.mail.smtp_password,
1053 from: config.mail.from,
1054 subject: config.mail.subject_prefix + "Your LiquidFeedback API alpha test account needs confirmation",
1055 to: member_notify_email,
1056 body: "\
1057 Hello " + member_name + ",\n\
1058 \n\
1059 thank you for registering at the public alpha test of the LiquidFeedback\n\
1060 application programming interface. To complete the registration process,\n\
1061 you need to confirm your email address by opening the following URL:\n\
1062 \n\
1063 " + config.public_url_path + "register_test_confirm?secret=" + member_notify_email_secret + "\n\
1064 \n\
1065 \n\
1066 After you've confirmed your email address, your account will be automatically\n\
1067 activated.\n\
1068 \n\
1069 Your account name is: " + member_name + "\n\
1070 \n\
1071 \n\
1072 You will need the following login and password to register and unregister\n\
1073 applications for your account later. This function is currently not\n\
1074 implemented, but please keep the credentials for future use.\n\
1075 \n\
1076 Account ID: " + member_id + "\n\
1077 Login: " + member_login + "\n\
1078 Password: " + member_password + "\n\
1079 \n\
1080 \n\
1081 To make you able to actually access the API interface, we added the following\n\
1082 application key with full member access privileges to your account:\n\
1083 \n\
1084 API Key: " + api_key_member + "\n\
1085 \n\
1086 \n\
1087 The base address of the public test is: " + config.public_url_path + "\n\
1088 \n\
1089 The programming interface is described in the LiquidFeedback API\n\
1090 specification: http://dev.liquidfeedback.org/trac/lf/wiki/API\n\
1091 \n\
1092 The current implementation status of lfapi is published at the LiquidFeedback\n\
1093 API server page: http://dev.liquidfeedback.org/trac/lf/wiki/lfapi\n\
1094 \n\
1095 If you have any questions or suggestions, please use our public mailing list\n\
1096 at http://dev.liquidfeedback.org/cgi-bin/mailman/listinfo/main\n\
1097 \n\
1098 For issues regarding your test account, contact us via email at\n\
1099 lqfb-maintainers@public-software-group.org\n\
1100 \n\
1101 \n\
1102 Sincerely,\n\
1103 \n\
1104 Your LiquidFeedback maintainers",
1105 },
1106 function(err, result){
1107 if(err){ console.log(err); }
1108 });
1110 respond('html', conn, req, res, 'ok', 'Account created. Please check your mailbox!<br /><br /><br /><a href="/">Back to start page</a>');
1111 });
1112 });
1113 });
1114 });
1115 },
1117 /*
1118 '/register': function (conn, req, res, params) {
1119 var invite_key = params.invite_key;
1120 var login = params.login;
1121 var password = params.password;
1122 var name = params.name;
1123 var notify_email = params.notify_email;
1124 if (!invite_key) {
1125 respond('json', conn, req, res, 'unprocessable', null, 'No invite_key supplied.');
1126 return;
1127 };
1128 if (!login) {
1129 respond('json', conn, req, res, 'unprocessable', null, 'No login supplied.');
1130 return;
1131 };
1132 if (!password) {
1133 respond('json', conn, req, res, 'unprocessable', null, 'No password supplied.');
1134 return;
1135 };
1136 if (!name) {
1137 respond('json', conn, req, res, 'unprocessable', null, 'No name supplied.');
1138 return;
1139 };
1140 if (!notify_email) {
1141 respond('json', conn, req, res, 'unprocessable', null, 'No notify_email supplied.');
1142 return;
1143 };
1144 // check if akey is valid and get member_id for akey
1145 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) {
1146 if (result.rows.length != 1) {
1147 respond('json', conn, req, res, 'forbidden', null, 'Supplied invite_key is not valid.');
1148 return;
1149 };
1150 var member_id = result.rows[0].id;
1151 // check if name is available
1152 db.query(conn, req, res, { select: ['NULL'], from: ['member'], where: ['member.name = ' + db.pgEncode(name)] }, function (result, conn) {
1153 if (result.rows.length > 0) {
1154 respond('json', conn, req, res, 'forbidden', null, 'Login name is not available, choose another one.');
1155 return;
1156 };
1157 // check if login is available
1158 db.query(conn, req, res, { select: ['NULL'], from: ['member'], where: ['member.login = ' + db.pgEncode(login)] }, function (result, conn) {
1159 if (result.rows.length > 0) {
1160 respond('json', conn, req, res, 'forbidden', null, 'Name is not available, choose another one.');
1161 return;
1162 };
1163 var query = { update: 'member', set: { activation: 'now', active: true, } };
1165 });
1166 });
1167 });
1168 },
1169 */
1171 '/session': function (conn, req, res, params) {
1172 var key = params.key;
1173 if (!key) {
1174 respond('json', conn, req, res, 'unprocessable', null, 'No application key supplied.');
1175 return;
1176 };
1177 var query = new selector.Selector();
1178 query.from('member');
1179 query.join('member_application', null, 'member_application.member_id = member.id');
1180 query.addField('member.id');
1181 query.addWhere(['member.active AND member_application.key = ?', key]);
1182 if (params.interactive) {
1183 query.forUpdateOf('member');
1185 db.query(conn, req, res, query, function (result, conn) {
1186 if (result.rows.length != 1) {
1187 respond('json', conn, req, res, 'forbidden', null, 'Supplied application key is not valid.');
1188 return;
1189 };
1190 var member_id = result.rows[0].id;
1191 var session_key = randomString(16);
1192 req.sessions[session_key] = member_id;
1193 var query;
1194 if (params.interactive) {
1195 query = new selector.SQLUpdate('member');
1196 query.addWhere(['member.id = ?', member_id]);
1197 query.addValues({ last_activity: 'now' });
1199 db.query(conn, req, res, query, function (result, conn) {
1200 respond('json', conn, req, res, 'ok', { session_key: session_key });
1201 });
1202 });
1203 },
1205 '/member': function (conn, req, res, params) {
1206 var fields = ['organizational_unit', 'internal_posts', 'realname', 'birthday', 'address', 'email', 'xmpp_address', 'website', 'phone', 'mobile_phone', 'profession', 'external_memberships', 'external_posts', 'statement']
1207 requireAccessLevel(conn, req, res, 'member', function() {
1208 var query = new selector.SQLUpdate('member');
1209 query.addWhere(['member.id = ?', req.current_member_id]);
1210 fields.forEach( function(field) {
1211 if (typeof(params[field]) != 'undefined') {
1212 query.addValues({ field: params[field] });
1213 } else {
1214 query.addValues({ field: null });
1216 });
1217 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1218 });
1219 },
1221 '/membership': function (conn, req, res, params) {
1222 requireAccessLevel(conn, req, res, 'member', function() {
1224 // check if area_id is set
1225 var area_id = parseInt(params.area_id);
1226 if (!area_id) {
1227 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an area_id.');
1228 return;
1231 // delete membership
1232 if (params.delete) {
1233 var query;
1234 query = new selector.SQLDelete('membership');
1235 query.addWhere(['area_id = ?', area_id]);
1236 query.addWhere(['member_id = ?', req.current_member_id]);
1237 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1239 // add membership
1240 } else {
1242 // lock member for upsert
1243 lockMemberById(conn, req, res, req.current_member_id, function() {
1245 // check and lock privilege
1246 requireAreaPrivilege(conn, req, res, area_id, function() {
1248 // upsert membership
1249 var query = new selector.Upserter('membership', ['area_id', 'member_id']);
1250 query.addValues({ area_id: area_id, member_id: req.current_member_id });
1251 db.query(conn, req, res, query, function(result) {
1252 respond('json', conn, req, res, 'ok');
1253 });
1254 });
1255 });
1257 });
1258 },
1260 '/interest': function (conn, req, res, params) {
1261 requireAccessLevel(conn, req, res, 'member', function() {
1262 var query;
1264 // check if issue_id is set
1265 var issue_id = parseInt(params.issue_id);
1266 if (!issue_id) {
1267 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1268 return;
1271 // lock member for upsert
1272 lockMemberById(conn, req, res, req.current_member_id, function() {
1274 // delete interest
1275 if (params.delete) {
1277 // check issue state
1278 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1280 // delete interest
1281 query = new selector.SQLDelete('interest');
1282 query.addWhere(['issue_id = ?', issue_id]);
1283 query.addWhere(['member_id = ?', req.current_member_id]);
1284 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1285 });
1287 // add interest
1288 } else {
1290 // check and lock privilege
1291 requireIssuePrivilege(conn, req, res, issue_id, function() {
1293 // check issue state
1294 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1296 // upsert interest
1297 var query = new selector.Upserter('interest', ['issue_id', 'member_id']);
1298 query.addValues({ issue_id: issue_id, member_id: req.current_member_id });
1299 db.query(conn, req, res, query, function(result) {
1300 respond('json', conn, req, res, 'ok');
1301 });
1302 });
1303 });
1304 };
1305 });
1306 });
1307 },
1309 '/issue_comment': function (conn, req, res, params) {
1310 requireAccessLevel(conn, req, res, 'member', function() {
1312 var issue_id = parseInt(params.issue_id);
1313 var formatting_engine = params.formatting_engine
1314 var content = params.content;
1316 if (!issue_id) {
1317 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1318 return;
1321 // delete issue comment
1322 if (params.delete) {
1323 var query;
1324 query = new selector.SQLDelete('issue_comment');
1325 query.addWhere(['issue_id = ?', params.issue_id]);
1326 query.addWhere(['member_id = ?', req.current_member_id]);
1327 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1329 // upsert issue comment
1330 } else {
1332 // check if formatting engine is supplied and valid
1333 if (!formatting_engine) {
1334 respond('json', conn, req, res, 'unprocessable', null, 'No formatting engine supplied.');
1335 return;
1336 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1337 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1338 return;
1339 };
1341 // check if content is supplied
1342 if (!content) {
1343 respond('json', conn, req, res, 'unprocessable', null, 'No content supplied.');
1344 return;
1347 // lock member for upsert
1348 lockMemberById(conn, req, res, req.current_member_id, function() {
1350 // check and lock privilege
1351 requireIssuePrivilege(conn, req, res, issue_id, function() {
1353 // upsert issue comment
1354 var query = new selector.Upserter('issue_comment', ['issue_id', 'member_id']);
1355 query.addValues({
1356 issue_id: issue_id,
1357 member_id: req.current_member_id,
1358 changed: 'now',
1359 formatting_engine: formatting_engine,
1360 content: content
1361 });
1363 db.query(conn, req, res, query, function(result) {
1364 respond('json', conn, req, res, 'ok');
1365 });
1367 });
1368 });
1372 });
1373 },
1375 '/voting_comment': function (conn, req, res, params) {
1376 requireAccessLevel(conn, req, res, 'member', function() {
1378 var issue_id = parseInt(params.issue_id);
1379 var formatting_engine = params.formatting_engine
1380 var content = params.content;
1382 if (!issue_id) {
1383 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an issue_id.');
1384 return;
1388 // delete voting comment
1389 if (params.delete) {
1390 var query;
1391 query = new selector.SQLDelete('voting_comment');
1392 query.addWhere(['issue_id = ?', params.issue_id]);
1393 query.addWhere(['member_id = ?', req.current_member_id]);
1394 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1396 // upsert voting comment
1397 } else {
1399 // check if formatting engine is supplied and valid
1400 if (!formatting_engine) {
1401 respond('json', conn, req, res, 'unprocessable', null, 'No formatting engine supplied.');
1402 return;
1403 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1404 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1405 return;
1406 };
1408 // check if content is supplied
1409 if (!content) {
1410 respond('json', conn, req, res, 'unprocessable', null, 'No content supplied.');
1411 return;
1414 // lock member for upsert
1415 lockMemberById(conn, req, res, req.current_member_id, function() {
1417 // check and lock privilege
1418 requireIssuePrivilege(conn, req, res, issue_id, function() {
1420 // check issue state
1421 requireIssueState(conn, req, res, issue_id, ['voting', 'finished_with_winner', 'finished_without_winner'], function() {
1423 // upsert voting comment
1424 var query = new selector.Upserter('voting_comment', ['issue_id', 'member_id']);
1425 query.addValues({
1426 issue_id: issue_id,
1427 member_id: req.current_member_id,
1428 changed: 'now',
1429 formatting_engine: formatting_engine,
1430 content: content
1431 });
1433 db.query(conn, req, res, query, function(result) {
1434 respond('json', conn, req, res, 'ok');
1435 });
1437 });
1438 });
1439 })
1440 };
1441 });
1442 },
1444 '/supporter': function (conn, req, res, params) {
1445 requireAccessLevel(conn, req, res, 'member', function() {
1446 var initiative_id = parseInt(params.initiative_id);
1447 var draft_id = parseInt(params.draft_id);
1449 // check if needed arguments are supplied
1450 if (!initiative_id) {
1451 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an initiative_id.');
1452 return;
1455 if (!draft_id) {
1456 respond('json', conn, req, res, 'unprocessable', null, 'You need to supply an draft_id.');
1457 return;
1460 // lock member for upsert
1461 lockMemberById(conn, req, res, req.current_member_id, function() {
1463 // delete supporter
1464 if (params.delete) {
1466 // check issue state
1467 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion', 'verification'], function() {
1469 // delete supporter
1470 var query = new selector.SQLDelete('supporter');
1471 query.addWhere(['initiative_id = ?', initiative_id]);
1472 query.addWhere(['member_id = ?', req.current_member_id]);
1473 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1475 });
1477 // upsert supporter
1478 } else {
1480 // check and lock privilege
1481 requireInitiativePrivilege(conn, req, res, initiative_id, function() {
1483 // check issue state
1484 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion', 'verification'], function() {
1486 // check if given draft is the current one
1487 var query = new selector.Selector('current_draft');
1488 query.addField('NULL');
1489 query.addWhere(['current_draft.initiative_id = ?', initiative_id]);
1490 query.addWhere(['current_draft.id = ?', draft_id]);
1492 db.query(conn, req, res, query, function(result) {
1493 if (result.rows.length != 1) {
1494 respond('json', conn, req, res, 'conflict', null, 'The draft with the supplied draft_id is not the current one anymore!');
1495 return;
1498 // upsert supporter
1499 var query = new selector.Upserter('supporter', ['initiative_id', 'member_id']);
1500 query.addValues({
1501 initiative_id: initiative_id,
1502 member_id: req.current_member_id,
1503 draft_id: draft_id
1504 });
1506 db.query(conn, req, res, query, function(result) {
1507 respond('json', conn, req, res, 'ok');
1508 });
1510 });
1511 });
1512 });
1513 };
1514 });
1515 });
1516 },
1518 '/draft': function (conn, req, res, params) {
1519 requireAccessLevel(conn, req, res, 'member', function() {
1520 var area_id = parseInt(params.area_id);
1521 var policy_id = parseInt(params.policy_id);
1522 var issue_id = parseInt(params.issue_id);
1523 var initiative_id = parseInt(params.initiative_id);
1524 var initiative_name = params.initiative_name;
1525 var initiative_discussion_url = params.initiative_discussion_url;
1526 var formatting_engine = params.formatting_engine;
1527 var content = params.content;
1529 if (!initiative_discussion_url) initiative_discussion_url = null;
1531 // check parameters
1532 if (!formatting_engine) {
1533 respond('json', conn, req, res, 'unprocessable', null, 'No formatting_engine supplied.');
1534 return;
1535 } else if (formatting_engine != 'rocketwiki' && formatting_engine != 'compat') {
1536 respond('json', conn, req, res, 'unprocessable', null, 'Invalid formatting engine supplied.');
1537 return;
1538 };
1540 if (!content) {
1541 respond('json', conn, req, res, 'unprocessable', null, 'No draft content supplied.');
1542 return;
1543 };
1545 lockMemberById(conn, req, res, req.current_member_id, function() {
1547 // new draft in new initiative in new issue
1548 if (area_id && !issue_id && !initiative_id) {
1550 // check parameters for new issue
1551 if (!policy_id) {
1552 respond('json', conn, req, res, 'unprocessable', null, 'No policy supplied.');
1553 return;
1556 if (!initiative_name) {
1557 respond('json', conn, req, res, 'unprocessable', null, 'No initiative name supplied.');
1558 return;
1561 requireAreaPrivilege(conn, req, res, area_id, function() {
1563 // check if policy is allowed in this area and if area and policy are active
1564 var query = new selector.Selector();
1565 query.from('allowed_policy');
1566 query.join('area', null, 'area.id = allowed_policy.area_id AND area.active');
1567 query.join('policy', null, 'policy.id = allowed_policy.policy_id AND policy.active');
1568 query.addField('NULL');
1569 query.addWhere(['area.id = ? AND policy.id = ?', area_id, policy_id]);
1570 db.query(conn, req, res, query, function (result, conn) {
1571 if (result.rows.length != 1) {
1572 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.');
1573 return;
1574 };
1576 // check contingent
1577 requireContingentLeft(conn, req, res, true, function() {
1579 // insert new issue
1580 var query = new selector.SQLInsert('issue');
1581 query.addValues({
1582 area_id: area_id,
1583 policy_id: policy_id
1584 });
1585 query.addReturning('id');
1586 db.query(conn, req, res, query, function(result) {
1587 var issue_id = result.rows[0].id;
1589 // insert new initiative
1590 var query = new selector.SQLInsert('initiative');
1591 query.addValues({
1592 issue_id: issue_id,
1593 name: initiative_name,
1594 discussion_url: initiative_discussion_url
1595 });
1596 query.addReturning('id');
1597 db.query(conn, req, res, query, function(result) {
1598 var initiative_id = result.rows[0].id;
1600 // insert initiator
1601 var query = new selector.SQLInsert('initiator');
1602 query.addValues({ initiative_id: initiative_id, member_id: req.current_member_id, accepted: true });
1603 db.query(conn, req, res, query, function(result) {
1605 // insert new draft
1606 var query = new selector.SQLInsert('draft');
1607 query.addValues({
1608 initiative_id: initiative_id,
1609 author_id: req.current_member_id,
1610 formatting_engine: formatting_engine,
1611 content: content
1612 });
1613 query.addReturning('id');
1614 db.query(conn, req, res, query, function (result, conn) {
1615 var draft_id = result.rows[0].id;
1617 respond('json', conn, req, res, 'ok', { issue_id: issue_id, initiative_id: initiative_id, draft_id: draft_id } );
1618 });
1619 });
1620 });
1621 });
1622 });
1623 });
1624 });
1626 // new draft in new initiative in existant issue
1627 } else if (issue_id && !area_id && !initiative_id) {
1629 if (!initiative_name) {
1630 respond('json', conn, req, res, 'unprocessable', null, 'No initiative name supplied.');
1631 return;
1634 // check privilege
1635 requireIssuePrivilege(conn, req, res, issue_id, function() {
1637 // check issue state
1638 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification'], function() {
1640 // check contingent
1641 requireContingentLeft(conn, req, res, true, function() {
1643 // insert initiative
1644 var query = new selector.SQLInsert('initiative');
1645 query.addValues({
1646 issue_id: issue_id,
1647 name: initiative_name,
1648 discussion_url: initiative_discussion_url
1649 });
1650 query.addReturning('id');
1651 db.query(conn, req, res, query, function(result) {
1652 var initiative_id = result.rows[0].id;
1654 // insert initiator
1655 var query = new selector.SQLInsert('initiator');
1656 query.addValues({
1657 initiative_id: initiative_id,
1658 member_id: req.current_member_id,
1659 accepted: true
1660 });
1661 db.query(conn, req, res, query, function(result) {
1663 // insert draft
1664 var query = new selector.SQLInsert('draft');
1665 query.addValues({
1666 initiative_id: initiative_id,
1667 author_id: req.current_member_id,
1668 formatting_engine: formatting_engine,
1669 content: content
1670 });
1671 query.addReturning('id');
1672 db.query(conn, req, res, query, function (result, conn) {
1674 var draft_id = result.rows[0].id;
1675 respond('json', conn, req, res, 'ok', { initiative_id: initiative_id, draft_id: draft_id } );
1677 });
1678 });
1679 });
1680 });
1681 });
1682 });
1684 // new draft in existant initiative
1685 } else if (initiative_id && !area_id && !issue_id ) {
1687 // check privilege
1688 requireInitiativePrivilege(conn, req, res, initiative_id, function() {
1690 // check issue state
1691 requireIssueStateForInitiative(conn, req, res, initiative_id, ['admission', 'discussion'], function() {
1694 // get initiator
1695 var query = new selector.Selector();
1696 query.from('initiator');
1697 query.addField('accepted');
1698 query.addWhere(['initiative_id = ? AND member_id = ?', initiative_id, req.current_member_id]);
1699 db.query(conn, req, res, query, function (result, conn) {
1701 // if member is not initiator, deny creating new draft
1702 if (result.rows.length != 1) {
1703 respond('json', conn, req, res, 'forbidden', null, 'You are not initiator of this initiative and not allowed to update its draft.');
1704 return;
1706 var initiator = result.rows[0];
1707 if (!initiator.accepted) {
1708 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.');
1709 return;
1710 };
1712 // check contingent
1713 requireContingentLeft(conn, req, res, false, function() {
1715 // insert new draft
1716 var query = new selector.SQLInsert('draft');
1717 query.addValues({
1718 initiative_id: initiative_id,
1719 author_id: req.current_member_id,
1720 formatting_engine: formatting_engine,
1721 content: content
1722 });
1723 query.addReturning('id');
1724 db.query(conn, req, res, query, function (result, conn) {
1726 var draft_id = result.rows[0].id;
1727 respond('json', conn, req, res, 'ok', { draft_id: draft_id } );
1728 });
1729 });
1730 });
1731 });
1732 });
1734 // none of them (invalid request)
1735 } else {
1736 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of area_id, issue_id or initiative_id must be supplied!');
1737 };
1739 });
1740 });
1741 },
1743 '/suggestion': function (conn, req, res, params) {
1744 requireAccessLevel(conn, req, res, 'member', function() {
1745 // TODO
1746 });
1747 },
1749 '/opinion': function (conn, req, res, params) {
1750 requireAccessLevel(conn, req, res, 'member', function() {
1751 // TODO
1752 });
1753 },
1755 '/delegation': function (conn, req, res, params) {
1756 requireAccessLevel(conn, req, res, 'member', function() {
1757 var unit_id = parseInt(params.unit_id);
1758 var area_id = parseInt(params.area_id);
1759 var issue_id = parseInt(params.issue_id);
1760 var trustee_id;
1762 if (params.trustee_id == '') {
1763 trustee_id = null;
1764 } else {
1765 trustee_id = parseInt(params.trustee_id);
1768 lockMemberById(conn, req, res, req.current_member_id, function() {
1770 if (params.delete) {
1771 var query = new selector.SQLDelete('delegation')
1772 if (unit_id && !area_id && !issue_id) {
1773 query.addWhere(['unit_id = ?', unit_id]);
1774 } else if (!unit_id && area_id && !issue_id) {
1775 query.addWhere(['area_id = ?', area_id]);
1776 } else if (!unit_id && !area_id && issue_id) {
1777 query.addWhere(['issue_id = ?', issue_id]);
1778 } else {
1779 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of unit, area_id, issue_id must be supplied!');
1780 return;
1782 query.addWhere(['truster_id = ?', req.current_member_id]);
1783 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1784 } else {
1785 var query = new selector.Upserter('delegation', ['truster_id']);
1786 query.addValues({
1787 truster_id: req.current_member_id,
1788 trustee_id: trustee_id
1789 });
1790 if (unit_id && !area_id && !issue_id) {
1792 // check privilege
1793 requireUnitPrivilege(conn, req, res, unit_id, function() {
1795 query.addKeys(['unit_id'])
1796 query.addValues({ unit_id: unit_id, scope: 'unit' });
1797 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1798 });
1800 } else if (!unit_id && area_id && !issue_id) {
1802 // check privilege
1803 requireAreaPrivilege(conn, req, res, area_id, function() {
1805 query.addKeys(['area_id'])
1806 query.addValues({ area_id: area_id, scope: 'area' });
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 requireIssuePrivilege(conn, req, res, issue_id, function() {
1815 // check issue state
1816 requireIssueState(conn, req, res, issue_id, ['admission', 'discussion', 'verification', 'voting'], function() {
1818 query.addKeys(['issue_id'])
1819 query.addValues({ issue_id: issue_id, scope: 'issue' });
1820 db.query(conn, req, res, query, function(result) { respond('json', conn, req, res, 'ok'); });
1821 });
1822 });
1823 } else {
1824 respond('json', conn, req, res, 'unprocessable', null, 'Excactly one of unit_id, area_id, issue_id must be supplied!');
1825 return;
1829 });
1831 });
1832 },
1834 };

Impressum / About Us