liquid_feedback_core

view core.sql @ 9:4af4df1415f9

Version beta10

Voting will be skipped, if no initiative is admitted for voting

Invite code feature allows people having an invite code to create one account

Contingent system to limit the number of initiatives or text entries to be submitted by each member within a given time

Ability to store a formatting engine for each draft, which can be used to allow initiatives to choose between available wiki parsers

New table setting storing user settings for the frontend (replaced hidden_hints column of beta9)

Better policy support:
- New table allowed_policy to select which policies can be used in each area
- Policies are now ordered by an index field

Bugfixes:
- In function close_voting(...): If there were no voters, this resulted in a NULL value as positive and negative vote counts
- In delete_private_data() function: Secret fields of member table are now deleted too
- Major bug fixed in lf_export, which caused data loss on execution
author jbe
date Thu Dec 10 12:00:00 2009 +0100 (2009-12-10)
parents e6faf5ff83af
children effdd7a04ea7
line source
2 CREATE LANGUAGE plpgsql; -- Triggers are implemented in PL/pgSQL
4 -- NOTE: In PostgreSQL every UNIQUE constraint implies creation of an index
6 BEGIN;
8 CREATE VIEW "liquid_feedback_version" AS
9 SELECT * FROM (VALUES ('beta10', NULL, NULL, NULL))
10 AS "subquery"("string", "major", "minor", "revision");
14 ----------------------
15 -- Full text search --
16 ----------------------
19 CREATE FUNCTION "text_search_query"("query_text_p" TEXT)
20 RETURNS TSQUERY
21 LANGUAGE 'plpgsql' IMMUTABLE AS $$
22 BEGIN
23 RETURN plainto_tsquery('pg_catalog.simple', "query_text_p");
24 END;
25 $$;
27 COMMENT ON FUNCTION "text_search_query"(TEXT) IS 'Usage: WHERE "text_search_data" @@ "text_search_query"(''<user query>'')';
30 CREATE FUNCTION "highlight"
31 ( "body_p" TEXT,
32 "query_text_p" TEXT )
33 RETURNS TEXT
34 LANGUAGE 'plpgsql' IMMUTABLE AS $$
35 BEGIN
36 RETURN ts_headline(
37 'pg_catalog.simple',
38 replace(replace("body_p", e'\\', e'\\\\'), '*', e'\\*'),
39 "text_search_query"("query_text_p"),
40 'StartSel=* StopSel=* HighlightAll=TRUE' );
41 END;
42 $$;
44 COMMENT ON FUNCTION "highlight"
45 ( "body_p" TEXT,
46 "query_text_p" TEXT )
47 IS 'For a given a user query this function encapsulates all matches with asterisks. Asterisks and backslashes being already present are preceeded with one extra backslash.';
51 -------------------------
52 -- Tables and indicies --
53 -------------------------
56 CREATE TABLE "member" (
57 "id" SERIAL4 PRIMARY KEY,
58 "login" TEXT NOT NULL UNIQUE,
59 "password" TEXT,
60 "active" BOOLEAN NOT NULL DEFAULT TRUE,
61 "admin" BOOLEAN NOT NULL DEFAULT FALSE,
62 "notify_email" TEXT,
63 "notify_email_confirmed" BOOLEAN,
64 "name" TEXT NOT NULL UNIQUE,
65 "identification" TEXT UNIQUE,
66 "organizational_unit" TEXT,
67 "internal_posts" TEXT,
68 "realname" TEXT,
69 "birthday" DATE,
70 "address" TEXT,
71 "email" TEXT,
72 "xmpp_address" TEXT,
73 "website" TEXT,
74 "phone" TEXT,
75 "mobile_phone" TEXT,
76 "profession" TEXT,
77 "external_memberships" TEXT,
78 "external_posts" TEXT,
79 "statement" TEXT,
80 "text_search_data" TSVECTOR,
81 CONSTRAINT "notify_email_null_check"
82 CHECK ("notify_email" NOTNULL = "notify_email_confirmed" NOTNULL) );
83 CREATE INDEX "member_active_idx" ON "member" ("active");
84 CREATE INDEX "member_text_search_data_idx" ON "member" USING gin ("text_search_data");
85 CREATE TRIGGER "update_text_search_data"
86 BEFORE INSERT OR UPDATE ON "member"
87 FOR EACH ROW EXECUTE PROCEDURE
88 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
89 "name", "identification", "organizational_unit", "internal_posts",
90 "realname", "external_memberships", "external_posts", "statement" );
92 COMMENT ON TABLE "member" IS 'Users of the system, e.g. members of an organization';
94 COMMENT ON COLUMN "member"."login" IS 'Login name';
95 COMMENT ON COLUMN "member"."password" IS 'Password (preferably as crypto-hash, depending on the frontend or access layer)';
96 COMMENT ON COLUMN "member"."active" IS 'Inactive members can not login and their supports/votes are not counted by the system.';
97 COMMENT ON COLUMN "member"."admin" IS 'TRUE for admins, which can administrate other users and setup policies and areas';
98 COMMENT ON COLUMN "member"."notify_email" IS 'Email address where notifications of the system are sent to';
99 COMMENT ON COLUMN "member"."notify_email_confirmed" IS 'TRUE, if "notify_email" has been confirmed';
100 COMMENT ON COLUMN "member"."name" IS 'Distinct name of the member';
101 COMMENT ON COLUMN "member"."identification" IS 'Optional identification number or code of the member';
102 COMMENT ON COLUMN "member"."organizational_unit" IS 'Branch or division of the organization the member belongs to';
103 COMMENT ON COLUMN "member"."internal_posts" IS 'Posts (offices) of the member inside the organization';
104 COMMENT ON COLUMN "member"."realname" IS 'Real name of the member, may be identical with "name"';
105 COMMENT ON COLUMN "member"."email" IS 'Published email address of the member; not used for system notifications';
106 COMMENT ON COLUMN "member"."external_memberships" IS 'Other organizations the member is involved in';
107 COMMENT ON COLUMN "member"."external_posts" IS 'Posts (offices) outside the organization';
108 COMMENT ON COLUMN "member"."statement" IS 'Freely chosen text of the member for his homepage within the system';
111 CREATE TABLE "invite_code" (
112 "code" TEXT PRIMARY KEY,
113 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
114 "used" TIMESTAMPTZ,
115 "member_id" INT4 UNIQUE REFERENCES "member" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
116 "comment" TEXT,
117 CONSTRAINT "only_used_codes_may_refer_to_member" CHECK ("used" NOTNULL OR "member_id" ISNULL) );
119 COMMENT ON TABLE "invite_code" IS 'Invite codes can be used once to create a new member account.';
121 COMMENT ON COLUMN "invite_code"."code" IS 'Secret code';
122 COMMENT ON COLUMN "invite_code"."created" IS 'Time of creation of the secret code';
123 COMMENT ON COLUMN "invite_code"."used" IS 'NULL, if not used yet, otherwise tells when this code was used to create a member account';
124 COMMENT ON COLUMN "invite_code"."member_id" IS 'References the member whose account was created with this code';
125 COMMENT ON COLUMN "invite_code"."comment" IS 'Comment on the code, which is to be used for administrative reasons only';
128 CREATE TABLE "setting" (
129 PRIMARY KEY ("member_id", "key"),
130 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
131 "key" TEXT NOT NULL,
132 "value" TEXT NOT NULL );
133 CREATE INDEX "setting_key_idx" ON "setting" ("key");
135 COMMENT ON TABLE "setting" IS 'Place to store frontend specific member settings';
137 COMMENT ON COLUMN "setting"."key" IS 'Name of the setting, preceded by a frontend specific prefix';
140 CREATE TYPE "member_image_type" AS ENUM ('photo', 'avatar');
142 COMMENT ON TYPE "member_image_type" IS 'Types of images for a member';
145 CREATE TABLE "member_image" (
146 PRIMARY KEY ("member_id", "image_type", "scaled"),
147 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
148 "image_type" "member_image_type",
149 "scaled" BOOLEAN,
150 "content_type" TEXT,
151 "data" BYTEA NOT NULL );
153 COMMENT ON TABLE "member_image" IS 'Images of members';
155 COMMENT ON COLUMN "member_image"."scaled" IS 'FALSE for original image, TRUE for scaled version of the image';
158 CREATE TABLE "member_count" (
159 "calculated" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
160 "total_count" INT4 NOT NULL );
162 COMMENT ON TABLE "member_count" IS 'Contains one row which contains the total count of active(!) members and a timestamp indicating when the total member count and area member counts were calculated';
164 COMMENT ON COLUMN "member_count"."calculated" IS 'timestamp indicating when the total member count and area member counts were calculated';
165 COMMENT ON COLUMN "member_count"."total_count" IS 'Total count of active(!) members';
168 CREATE TABLE "contact" (
169 PRIMARY KEY ("member_id", "other_member_id"),
170 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
171 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
172 "public" BOOLEAN NOT NULL DEFAULT FALSE );
174 COMMENT ON TABLE "contact" IS 'Contact lists';
176 COMMENT ON COLUMN "contact"."member_id" IS 'Member having the contact list';
177 COMMENT ON COLUMN "contact"."other_member_id" IS 'Member referenced in the contact list';
178 COMMENT ON COLUMN "contact"."public" IS 'TRUE = display contact publically';
181 CREATE TABLE "session" (
182 "ident" TEXT PRIMARY KEY,
183 "additional_secret" TEXT,
184 "expiry" TIMESTAMPTZ NOT NULL DEFAULT now() + '24 hours',
185 "member_id" INT8 REFERENCES "member" ("id") ON DELETE SET NULL,
186 "lang" TEXT );
187 CREATE INDEX "session_expiry_idx" ON "session" ("expiry");
189 COMMENT ON TABLE "session" IS 'Sessions, i.e. for a web-frontend';
191 COMMENT ON COLUMN "session"."ident" IS 'Secret session identifier (i.e. random string)';
192 COMMENT ON COLUMN "session"."additional_secret" IS 'Additional field to store a secret, which can be used against CSRF attacks';
193 COMMENT ON COLUMN "session"."member_id" IS 'Reference to member, who is logged in';
194 COMMENT ON COLUMN "session"."lang" IS 'Language code of the selected language';
197 CREATE TABLE "policy" (
198 "id" SERIAL4 PRIMARY KEY,
199 "index" INT4 NOT NULL,
200 "active" BOOLEAN NOT NULL DEFAULT TRUE,
201 "name" TEXT NOT NULL UNIQUE,
202 "description" TEXT NOT NULL DEFAULT '',
203 "admission_time" INTERVAL NOT NULL,
204 "discussion_time" INTERVAL NOT NULL,
205 "verification_time" INTERVAL NOT NULL,
206 "voting_time" INTERVAL NOT NULL,
207 "issue_quorum_num" INT4 NOT NULL,
208 "issue_quorum_den" INT4 NOT NULL,
209 "initiative_quorum_num" INT4 NOT NULL,
210 "initiative_quorum_den" INT4 NOT NULL );
211 CREATE INDEX "policy_active_idx" ON "policy" ("active");
213 COMMENT ON TABLE "policy" IS 'Policies for a particular proceeding type (timelimits, quorum)';
215 COMMENT ON COLUMN "policy"."index" IS 'Determines the order in listings';
216 COMMENT ON COLUMN "policy"."active" IS 'TRUE = policy can be used for new issues';
217 COMMENT ON COLUMN "policy"."admission_time" IS 'Maximum time an issue stays open without being "accepted"';
218 COMMENT ON COLUMN "policy"."discussion_time" IS 'Regular time until an issue is "half_frozen" after being "accepted"';
219 COMMENT ON COLUMN "policy"."verification_time" IS 'Regular time until an issue is "fully_frozen" after being "half_frozen"';
220 COMMENT ON COLUMN "policy"."voting_time" IS 'Time after an issue is "fully_frozen" but not "closed"';
221 COMMENT ON COLUMN "policy"."issue_quorum_num" IS 'Numerator of quorum to be reached by one initiative of an issue to be "accepted"';
222 COMMENT ON COLUMN "policy"."issue_quorum_den" IS 'Denominator of quorum to be reached by one initiative of an issue to be "accepted"';
223 COMMENT ON COLUMN "policy"."initiative_quorum_num" IS 'Numerator of quorum to be reached by an initiative to be "admitted" for voting';
224 COMMENT ON COLUMN "policy"."initiative_quorum_den" IS 'Denominator of quorum to be reached by an initiative to be "admitted" for voting';
227 CREATE TABLE "area" (
228 "id" SERIAL4 PRIMARY KEY,
229 "active" BOOLEAN NOT NULL DEFAULT TRUE,
230 "name" TEXT NOT NULL,
231 "description" TEXT NOT NULL DEFAULT '',
232 "direct_member_count" INT4,
233 "member_weight" INT4,
234 "autoreject_weight" INT4,
235 "text_search_data" TSVECTOR );
236 CREATE INDEX "area_active_idx" ON "area" ("active");
237 CREATE INDEX "area_text_search_data_idx" ON "area" USING gin ("text_search_data");
238 CREATE TRIGGER "update_text_search_data"
239 BEFORE INSERT OR UPDATE ON "area"
240 FOR EACH ROW EXECUTE PROCEDURE
241 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
242 "name", "description" );
244 COMMENT ON TABLE "area" IS 'Subject areas';
246 COMMENT ON COLUMN "area"."active" IS 'TRUE means new issues can be created in this area';
247 COMMENT ON COLUMN "area"."direct_member_count" IS 'Number of active members of that area (ignoring their weight), as calculated from view "area_member_count"';
248 COMMENT ON COLUMN "area"."member_weight" IS 'Same as "direct_member_count" but respecting delegations';
249 COMMENT ON COLUMN "area"."autoreject_weight" IS 'Sum of weight of members using the autoreject feature';
252 CREATE TABLE "allowed_policy" (
253 PRIMARY KEY ("area_id", "policy_id"),
254 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
255 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
256 "default_policy" BOOLEAN NOT NULL DEFAULT FALSE );
257 CREATE UNIQUE INDEX "allowed_policy_one_default_per_area_idx" ON "allowed_policy" ("area_id") WHERE "default_policy";
259 COMMENT ON TABLE "allowed_policy" IS 'Selects which policies can be used in each area';
261 COMMENT ON COLUMN "allowed_policy"."default_policy" IS 'One policy per area can be set as default.';
264 CREATE TYPE "snapshot_event" AS ENUM ('periodic', 'end_of_admission', 'start_of_voting');
266 COMMENT ON TYPE "snapshot_event" IS 'Reason for snapshots: ''periodic'' = due to periodic recalculation, ''end_of_admission'' = saved state at end of admission period, ''start_of_voting'' = saved state at end of verification period';
269 CREATE TABLE "issue" (
270 "id" SERIAL4 PRIMARY KEY,
271 "area_id" INT4 NOT NULL REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
272 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
273 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
274 "accepted" TIMESTAMPTZ,
275 "half_frozen" TIMESTAMPTZ,
276 "fully_frozen" TIMESTAMPTZ,
277 "closed" TIMESTAMPTZ,
278 "ranks_available" BOOLEAN NOT NULL DEFAULT FALSE,
279 "snapshot" TIMESTAMPTZ,
280 "latest_snapshot_event" "snapshot_event",
281 "population" INT4,
282 "vote_now" INT4,
283 "vote_later" INT4,
284 "voter_count" INT4,
285 CONSTRAINT "valid_state" CHECK (
286 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
287 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
288 ("accepted" NOTNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
289 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
290 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
291 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
292 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = TRUE) ),
293 CONSTRAINT "state_change_order" CHECK (
294 "created" <= "accepted" AND
295 "accepted" <= "half_frozen" AND
296 "half_frozen" <= "fully_frozen" AND
297 "fully_frozen" <= "closed" ),
298 CONSTRAINT "last_snapshot_on_full_freeze" CHECK ("snapshot" = "fully_frozen"), -- NOTE: snapshot can be set, while frozen is NULL yet
299 CONSTRAINT "freeze_requires_snapshot" CHECK ("fully_frozen" ISNULL OR "snapshot" NOTNULL),
300 CONSTRAINT "set_both_or_none_of_snapshot_and_latest_snapshot_event" CHECK ("snapshot" NOTNULL = "latest_snapshot_event" NOTNULL) );
301 CREATE INDEX "issue_area_id_idx" ON "issue" ("area_id");
302 CREATE INDEX "issue_policy_id_idx" ON "issue" ("policy_id");
303 CREATE INDEX "issue_created_idx_open" ON "issue" ("created") WHERE "closed" ISNULL;
305 COMMENT ON TABLE "issue" IS 'Groups of initiatives';
307 COMMENT ON COLUMN "issue"."accepted" IS 'Point in time, when one initiative of issue reached the "issue_quorum"';
308 COMMENT ON COLUMN "issue"."half_frozen" IS 'Point in time, when "discussion_time" has elapsed, or members voted for voting';
309 COMMENT ON COLUMN "issue"."fully_frozen" IS 'Point in time, when "verification_time" has elapsed';
310 COMMENT ON COLUMN "issue"."closed" IS 'Point in time, when "admission_time" or "voting_time" have elapsed, and issue is no longer active';
311 COMMENT ON COLUMN "issue"."ranks_available" IS 'TRUE = ranks have been calculated';
312 COMMENT ON COLUMN "issue"."snapshot" IS 'Point in time, when snapshot tables have been updated and "population", "vote_now", "vote_later" and *_count values were precalculated';
313 COMMENT ON COLUMN "issue"."latest_snapshot_event" IS 'Event type of latest snapshot for issue; Can be used to select the latest snapshot data in the snapshot tables';
314 COMMENT ON COLUMN "issue"."population" IS 'Sum of "weight" column in table "direct_population_snapshot"';
315 COMMENT ON COLUMN "issue"."vote_now" IS 'Number of votes in favor of voting now, as calculated from table "direct_interest_snapshot"';
316 COMMENT ON COLUMN "issue"."vote_later" IS 'Number of votes against voting now, as calculated from table "direct_interest_snapshot"';
317 COMMENT ON COLUMN "issue"."voter_count" IS 'Total number of direct and delegating voters; This value is related to the final voting, while "population" is related to snapshots before the final voting';
320 CREATE TABLE "initiative" (
321 UNIQUE ("issue_id", "id"), -- index needed for foreign-key on table "vote"
322 "issue_id" INT4 NOT NULL REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
323 "id" SERIAL4 PRIMARY KEY,
324 "name" TEXT NOT NULL,
325 "discussion_url" TEXT,
326 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
327 "revoked" TIMESTAMPTZ,
328 "admitted" BOOLEAN,
329 "supporter_count" INT4,
330 "informed_supporter_count" INT4,
331 "satisfied_supporter_count" INT4,
332 "satisfied_informed_supporter_count" INT4,
333 "positive_votes" INT4,
334 "negative_votes" INT4,
335 "rank" INT4,
336 "text_search_data" TSVECTOR,
337 CONSTRAINT "revoked_initiatives_cant_be_admitted"
338 CHECK ("revoked" ISNULL OR "admitted" ISNULL),
339 CONSTRAINT "non_admitted_initiatives_cant_contain_voting_results"
340 CHECK ("admitted" = TRUE OR ("positive_votes" ISNULL AND "negative_votes" ISNULL AND "rank" ISNULL)) );
341 CREATE INDEX "initiative_text_search_data_idx" ON "initiative" USING gin ("text_search_data");
342 CREATE TRIGGER "update_text_search_data"
343 BEFORE INSERT OR UPDATE ON "initiative"
344 FOR EACH ROW EXECUTE PROCEDURE
345 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
346 "name", "discussion_url");
348 COMMENT ON TABLE "initiative" IS 'Group of members publishing drafts for resolutions to be passed';
350 COMMENT ON COLUMN "initiative"."discussion_url" IS 'URL pointing to a discussion platform for this initiative';
351 COMMENT ON COLUMN "initiative"."revoked" IS 'Point in time, when one initiator decided to revoke the initiative';
352 COMMENT ON COLUMN "initiative"."admitted" IS 'True, if initiative reaches the "initiative_quorum" when freezing the issue';
353 COMMENT ON COLUMN "initiative"."supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
354 COMMENT ON COLUMN "initiative"."informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
355 COMMENT ON COLUMN "initiative"."satisfied_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
356 COMMENT ON COLUMN "initiative"."satisfied_informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
357 COMMENT ON COLUMN "initiative"."positive_votes" IS 'Calculated from table "direct_voter"';
358 COMMENT ON COLUMN "initiative"."negative_votes" IS 'Calculated from table "direct_voter"';
359 COMMENT ON COLUMN "initiative"."rank" IS 'Rank of approved initiatives (winner is 1), calculated from table "direct_voter"';
362 CREATE TABLE "draft" (
363 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "supporter"
364 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
365 "id" SERIAL8 PRIMARY KEY,
366 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
367 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
368 "formatting_engine" TEXT,
369 "content" TEXT NOT NULL,
370 "text_search_data" TSVECTOR );
371 CREATE INDEX "draft_author_id_created_idx" ON "draft" ("author_id", "created");
372 CREATE INDEX "draft_text_search_data_idx" ON "draft" USING gin ("text_search_data");
373 CREATE TRIGGER "update_text_search_data"
374 BEFORE INSERT OR UPDATE ON "draft"
375 FOR EACH ROW EXECUTE PROCEDURE
376 tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "content");
378 COMMENT ON TABLE "draft" IS 'Drafts of initiatives to solve issues';
380 COMMENT ON COLUMN "draft"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used';
381 COMMENT ON COLUMN "draft"."content" IS 'Text of the draft in a format depending on the field "formatting_engine"';
384 CREATE TABLE "suggestion" (
385 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "opinion"
386 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
387 "id" SERIAL8 PRIMARY KEY,
388 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
389 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
390 "name" TEXT NOT NULL,
391 "description" TEXT NOT NULL DEFAULT '',
392 "text_search_data" TSVECTOR,
393 "minus2_unfulfilled_count" INT4,
394 "minus2_fulfilled_count" INT4,
395 "minus1_unfulfilled_count" INT4,
396 "minus1_fulfilled_count" INT4,
397 "plus1_unfulfilled_count" INT4,
398 "plus1_fulfilled_count" INT4,
399 "plus2_unfulfilled_count" INT4,
400 "plus2_fulfilled_count" INT4 );
401 CREATE INDEX "suggestion_author_id_created_idx" ON "suggestion" ("author_id", "created");
402 CREATE INDEX "suggestion_text_search_data_idx" ON "suggestion" USING gin ("text_search_data");
403 CREATE TRIGGER "update_text_search_data"
404 BEFORE INSERT OR UPDATE ON "suggestion"
405 FOR EACH ROW EXECUTE PROCEDURE
406 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
407 "name", "description");
409 COMMENT ON TABLE "suggestion" IS 'Suggestions to initiators, to change the current draft';
411 COMMENT ON COLUMN "suggestion"."minus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
412 COMMENT ON COLUMN "suggestion"."minus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
413 COMMENT ON COLUMN "suggestion"."minus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
414 COMMENT ON COLUMN "suggestion"."minus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
415 COMMENT ON COLUMN "suggestion"."plus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
416 COMMENT ON COLUMN "suggestion"."plus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
417 COMMENT ON COLUMN "suggestion"."plus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
418 COMMENT ON COLUMN "suggestion"."plus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
421 CREATE TABLE "membership" (
422 PRIMARY KEY ("area_id", "member_id"),
423 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
424 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
425 "autoreject" BOOLEAN NOT NULL DEFAULT FALSE );
426 CREATE INDEX "membership_member_id_idx" ON "membership" ("member_id");
428 COMMENT ON TABLE "membership" IS 'Interest of members in topic areas';
430 COMMENT ON COLUMN "membership"."autoreject" IS 'TRUE = member votes against all initiatives in case of not explicitly taking part in the voting procedure; If there exists an "interest" entry, the interest entry has precedence';
433 CREATE TABLE "interest" (
434 PRIMARY KEY ("issue_id", "member_id"),
435 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
436 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
437 "autoreject" BOOLEAN NOT NULL,
438 "voting_requested" BOOLEAN );
439 CREATE INDEX "interest_member_id_idx" ON "interest" ("member_id");
441 COMMENT ON TABLE "interest" IS 'Interest of members in a particular issue';
443 COMMENT ON COLUMN "interest"."autoreject" IS 'TRUE = member votes against all initiatives in case of not explicitly taking part in the voting procedure';
444 COMMENT ON COLUMN "interest"."voting_requested" IS 'TRUE = member wants to vote now, FALSE = member wants to vote later, NULL = policy rules should apply';
447 CREATE TABLE "initiator" (
448 PRIMARY KEY ("initiative_id", "member_id"),
449 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
450 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
451 "accepted" BOOLEAN NOT NULL DEFAULT TRUE );
452 CREATE INDEX "initiator_member_id_idx" ON "initiator" ("member_id");
454 COMMENT ON TABLE "initiator" IS 'Members who are allowed to post new drafts';
456 COMMENT ON COLUMN "initiator"."accepted" IS 'If "accepted" = FALSE, then the member was invited to be a co-initiator, but has not answered yet.';
459 CREATE TABLE "supporter" (
460 "issue_id" INT4 NOT NULL,
461 PRIMARY KEY ("initiative_id", "member_id"),
462 "initiative_id" INT4,
463 "member_id" INT4,
464 "draft_id" INT8 NOT NULL,
465 FOREIGN KEY ("issue_id", "member_id") REFERENCES "interest" ("issue_id", "member_id") ON DELETE RESTRICT ON UPDATE CASCADE,
466 FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE CASCADE ON UPDATE CASCADE );
467 CREATE INDEX "supporter_member_id_idx" ON "supporter" ("member_id");
469 COMMENT ON TABLE "supporter" IS 'Members who support an initiative (conditionally)';
471 COMMENT ON COLUMN "supporter"."draft_id" IS 'Latest seen draft, defaults to current draft of the initiative (implemented by trigger "default_for_draft_id")';
474 CREATE TABLE "opinion" (
475 "initiative_id" INT4 NOT NULL,
476 PRIMARY KEY ("suggestion_id", "member_id"),
477 "suggestion_id" INT8,
478 "member_id" INT4,
479 "degree" INT2 NOT NULL CHECK ("degree" >= -2 AND "degree" <= 2 AND "degree" != 0),
480 "fulfilled" BOOLEAN NOT NULL DEFAULT FALSE,
481 FOREIGN KEY ("initiative_id", "suggestion_id") REFERENCES "suggestion" ("initiative_id", "id") ON DELETE RESTRICT ON UPDATE CASCADE,
482 FOREIGN KEY ("initiative_id", "member_id") REFERENCES "supporter" ("initiative_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
483 CREATE INDEX "opinion_member_id_idx" ON "opinion" ("member_id");
485 COMMENT ON TABLE "opinion" IS 'Opinion on suggestions (criticism related to initiatives)';
487 COMMENT ON COLUMN "opinion"."degree" IS '2 = fulfillment required for support; 1 = fulfillment desired; -1 = fulfillment unwanted; -2 = fulfillment cancels support';
490 CREATE TABLE "delegation" (
491 "id" SERIAL8 PRIMARY KEY,
492 "truster_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
493 "trustee_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
494 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
495 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
496 CONSTRAINT "cant_delegate_to_yourself" CHECK ("truster_id" != "trustee_id"),
497 CONSTRAINT "area_id_or_issue_id_can_be_set_but_not_both" CHECK ("area_id" ISNULL OR "issue_id" ISNULL),
498 UNIQUE ("area_id", "truster_id", "trustee_id"),
499 UNIQUE ("issue_id", "truster_id", "trustee_id") );
500 CREATE UNIQUE INDEX "delegation_default_truster_id_trustee_id_unique_idx"
501 ON "delegation" ("truster_id", "trustee_id")
502 WHERE "area_id" ISNULL AND "issue_id" ISNULL;
503 CREATE INDEX "delegation_truster_id_idx" ON "delegation" ("truster_id");
504 CREATE INDEX "delegation_trustee_id_idx" ON "delegation" ("trustee_id");
506 COMMENT ON TABLE "delegation" IS 'Delegation of vote-weight to other members';
508 COMMENT ON COLUMN "delegation"."area_id" IS 'Reference to area, if delegation is area-wide, otherwise NULL';
509 COMMENT ON COLUMN "delegation"."issue_id" IS 'Reference to issue, if delegation is issue-wide, otherwise NULL';
512 CREATE TABLE "direct_population_snapshot" (
513 PRIMARY KEY ("issue_id", "event", "member_id"),
514 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
515 "event" "snapshot_event",
516 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
517 "weight" INT4,
518 "interest_exists" BOOLEAN NOT NULL );
519 CREATE INDEX "direct_population_snapshot_member_id_idx" ON "direct_population_snapshot" ("member_id");
521 COMMENT ON TABLE "direct_population_snapshot" IS 'Snapshot of active members having either a "membership" in the "area" or an "interest" in the "issue"';
523 COMMENT ON COLUMN "direct_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
524 COMMENT ON COLUMN "direct_population_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_population_snapshot"';
525 COMMENT ON COLUMN "direct_population_snapshot"."interest_exists" IS 'TRUE if entry is due to interest in issue, FALSE if entry is only due to membership in area';
528 CREATE TABLE "delegating_population_snapshot" (
529 PRIMARY KEY ("issue_id", "event", "member_id"),
530 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
531 "event" "snapshot_event",
532 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
533 "weight" INT4,
534 "delegate_member_ids" INT4[] NOT NULL );
535 CREATE INDEX "delegating_population_snapshot_member_id_idx" ON "delegating_population_snapshot" ("member_id");
537 COMMENT ON TABLE "direct_population_snapshot" IS 'Delegations increasing the weight of entries in the "direct_population_snapshot" table';
539 COMMENT ON COLUMN "delegating_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
540 COMMENT ON COLUMN "delegating_population_snapshot"."member_id" IS 'Delegating member';
541 COMMENT ON COLUMN "delegating_population_snapshot"."weight" IS 'Intermediate weight';
542 COMMENT ON COLUMN "delegating_population_snapshot"."delegate_member_ids" IS 'Chain of members who act as delegates; last entry referes to "member_id" column of table "direct_population_snapshot"';
545 CREATE TABLE "direct_interest_snapshot" (
546 PRIMARY KEY ("issue_id", "event", "member_id"),
547 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
548 "event" "snapshot_event",
549 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
550 "weight" INT4,
551 "voting_requested" BOOLEAN );
552 CREATE INDEX "direct_interest_snapshot_member_id_idx" ON "direct_interest_snapshot" ("member_id");
554 COMMENT ON TABLE "direct_interest_snapshot" IS 'Snapshot of active members having an "interest" in the "issue"';
556 COMMENT ON COLUMN "direct_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
557 COMMENT ON COLUMN "direct_interest_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_interest_snapshot"';
558 COMMENT ON COLUMN "direct_interest_snapshot"."voting_requested" IS 'Copied from column "voting_requested" of table "interest"';
561 CREATE TABLE "delegating_interest_snapshot" (
562 PRIMARY KEY ("issue_id", "event", "member_id"),
563 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
564 "event" "snapshot_event",
565 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
566 "weight" INT4,
567 "delegate_member_ids" INT4[] NOT NULL );
568 CREATE INDEX "delegating_interest_snapshot_member_id_idx" ON "delegating_interest_snapshot" ("member_id");
570 COMMENT ON TABLE "delegating_interest_snapshot" IS 'Delegations increasing the weight of entries in the "direct_interest_snapshot" table';
572 COMMENT ON COLUMN "delegating_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
573 COMMENT ON COLUMN "delegating_interest_snapshot"."member_id" IS 'Delegating member';
574 COMMENT ON COLUMN "delegating_interest_snapshot"."weight" IS 'Intermediate weight';
575 COMMENT ON COLUMN "delegating_interest_snapshot"."delegate_member_ids" IS 'Chain of members who act as delegates; last entry referes to "member_id" column of table "direct_interest_snapshot"';
578 CREATE TABLE "direct_supporter_snapshot" (
579 "issue_id" INT4 NOT NULL,
580 PRIMARY KEY ("initiative_id", "event", "member_id"),
581 "initiative_id" INT4,
582 "event" "snapshot_event",
583 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
584 "informed" BOOLEAN NOT NULL,
585 "satisfied" BOOLEAN NOT NULL,
586 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
587 FOREIGN KEY ("issue_id", "event", "member_id") REFERENCES "direct_interest_snapshot" ("issue_id", "event", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
588 CREATE INDEX "direct_supporter_snapshot_member_id_idx" ON "direct_supporter_snapshot" ("member_id");
590 COMMENT ON TABLE "direct_supporter_snapshot" IS 'Snapshot of supporters of initiatives (weight is stored in "direct_interest_snapshot")';
592 COMMENT ON COLUMN "direct_supporter_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
593 COMMENT ON COLUMN "direct_supporter_snapshot"."informed" IS 'Supporter has seen the latest draft of the initiative';
594 COMMENT ON COLUMN "direct_supporter_snapshot"."satisfied" IS 'Supporter has no "critical_opinion"s';
597 CREATE TABLE "direct_voter" (
598 PRIMARY KEY ("issue_id", "member_id"),
599 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
600 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
601 "weight" INT4,
602 "autoreject" BOOLEAN NOT NULL DEFAULT FALSE );
603 CREATE INDEX "direct_voter_member_id_idx" ON "direct_voter" ("member_id");
605 COMMENT ON TABLE "direct_voter" IS 'Members having directly voted for/against initiatives of an issue';
607 COMMENT ON COLUMN "direct_voter"."weight" IS 'Weight of member (1 or higher) according to "delegating_voter" table';
608 COMMENT ON COLUMN "direct_voter"."autoreject" IS 'Votes were inserted due to "autoreject" feature';
611 CREATE TABLE "delegating_voter" (
612 PRIMARY KEY ("issue_id", "member_id"),
613 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
614 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
615 "weight" INT4,
616 "delegate_member_ids" INT4[] NOT NULL );
617 CREATE INDEX "delegating_voter_member_id_idx" ON "direct_voter" ("member_id");
619 COMMENT ON TABLE "delegating_voter" IS 'Delegations increasing the weight of entries in the "direct_voter" table';
621 COMMENT ON COLUMN "delegating_voter"."member_id" IS 'Delegating member';
622 COMMENT ON COLUMN "delegating_voter"."weight" IS 'Intermediate weight';
623 COMMENT ON COLUMN "delegating_voter"."delegate_member_ids" IS 'Chain of members who act as delegates; last entry referes to "member_id" column of table "direct_voter"';
626 CREATE TABLE "vote" (
627 "issue_id" INT4 NOT NULL,
628 PRIMARY KEY ("initiative_id", "member_id"),
629 "initiative_id" INT4,
630 "member_id" INT4,
631 "grade" INT4,
632 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
633 FOREIGN KEY ("issue_id", "member_id") REFERENCES "direct_voter" ("issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
634 CREATE INDEX "vote_member_id_idx" ON "vote" ("member_id");
636 COMMENT ON TABLE "vote" IS 'Manual and delegated votes without abstentions';
638 COMMENT ON COLUMN "vote"."grade" IS 'Values smaller than zero mean reject, values greater than zero mean acceptance, zero or missing row means abstention. Preferences are expressed by different positive or negative numbers.';
641 CREATE TABLE "contingent" (
642 "time_frame" INTERVAL PRIMARY KEY,
643 "text_entry_limit" INT4,
644 "initiative_limit" INT4 );
646 COMMENT ON TABLE "contingent" IS 'Amount of text entries or initiatives a user may create within a given time frame. Only one row needs to be fulfilled for a member to be allowed to post. This table must not be empty.';
648 COMMENT ON COLUMN "contingent"."text_entry_limit" IS 'Number of new drafts or suggestions to be submitted by each member within the given time frame';
649 COMMENT ON COLUMN "contingent"."initiative_limit" IS 'Number of new initiatives to be opened by each member within a given time frame';
653 ----------------------------
654 -- Additional constraints --
655 ----------------------------
658 CREATE FUNCTION "issue_requires_first_initiative_trigger"()
659 RETURNS TRIGGER
660 LANGUAGE 'plpgsql' VOLATILE AS $$
661 BEGIN
662 IF NOT EXISTS (
663 SELECT NULL FROM "initiative" WHERE "issue_id" = NEW."id"
664 ) THEN
665 --RAISE 'Cannot create issue without an initial initiative.' USING
666 -- ERRCODE = 'integrity_constraint_violation',
667 -- HINT = 'Create issue, initiative, and draft within the same transaction.';
668 RAISE EXCEPTION 'Cannot create issue without an initial initiative.';
669 END IF;
670 RETURN NULL;
671 END;
672 $$;
674 CREATE CONSTRAINT TRIGGER "issue_requires_first_initiative"
675 AFTER INSERT OR UPDATE ON "issue" DEFERRABLE INITIALLY DEFERRED
676 FOR EACH ROW EXECUTE PROCEDURE
677 "issue_requires_first_initiative_trigger"();
679 COMMENT ON FUNCTION "issue_requires_first_initiative_trigger"() IS 'Implementation of trigger "issue_requires_first_initiative" on table "issue"';
680 COMMENT ON TRIGGER "issue_requires_first_initiative" ON "issue" IS 'Ensure that new issues have at least one initiative';
683 CREATE FUNCTION "last_initiative_deletes_issue_trigger"()
684 RETURNS TRIGGER
685 LANGUAGE 'plpgsql' VOLATILE AS $$
686 DECLARE
687 "reference_lost" BOOLEAN;
688 BEGIN
689 IF TG_OP = 'DELETE' THEN
690 "reference_lost" := TRUE;
691 ELSE
692 "reference_lost" := NEW."issue_id" != OLD."issue_id";
693 END IF;
694 IF
695 "reference_lost" AND NOT EXISTS (
696 SELECT NULL FROM "initiative" WHERE "issue_id" = OLD."issue_id"
697 )
698 THEN
699 DELETE FROM "issue" WHERE "id" = OLD."issue_id";
700 END IF;
701 RETURN NULL;
702 END;
703 $$;
705 CREATE CONSTRAINT TRIGGER "last_initiative_deletes_issue"
706 AFTER UPDATE OR DELETE ON "initiative" DEFERRABLE INITIALLY DEFERRED
707 FOR EACH ROW EXECUTE PROCEDURE
708 "last_initiative_deletes_issue_trigger"();
710 COMMENT ON FUNCTION "last_initiative_deletes_issue_trigger"() IS 'Implementation of trigger "last_initiative_deletes_issue" on table "initiative"';
711 COMMENT ON TRIGGER "last_initiative_deletes_issue" ON "initiative" IS 'Removing the last initiative of an issue deletes the issue';
714 CREATE FUNCTION "initiative_requires_first_draft_trigger"()
715 RETURNS TRIGGER
716 LANGUAGE 'plpgsql' VOLATILE AS $$
717 BEGIN
718 IF NOT EXISTS (
719 SELECT NULL FROM "draft" WHERE "initiative_id" = NEW."id"
720 ) THEN
721 --RAISE 'Cannot create initiative without an initial draft.' USING
722 -- ERRCODE = 'integrity_constraint_violation',
723 -- HINT = 'Create issue, initiative and draft within the same transaction.';
724 RAISE EXCEPTION 'Cannot create initiative without an initial draft.';
725 END IF;
726 RETURN NULL;
727 END;
728 $$;
730 CREATE CONSTRAINT TRIGGER "initiative_requires_first_draft"
731 AFTER INSERT OR UPDATE ON "initiative" DEFERRABLE INITIALLY DEFERRED
732 FOR EACH ROW EXECUTE PROCEDURE
733 "initiative_requires_first_draft_trigger"();
735 COMMENT ON FUNCTION "initiative_requires_first_draft_trigger"() IS 'Implementation of trigger "initiative_requires_first_draft" on table "initiative"';
736 COMMENT ON TRIGGER "initiative_requires_first_draft" ON "initiative" IS 'Ensure that new initiatives have at least one draft';
739 CREATE FUNCTION "last_draft_deletes_initiative_trigger"()
740 RETURNS TRIGGER
741 LANGUAGE 'plpgsql' VOLATILE AS $$
742 DECLARE
743 "reference_lost" BOOLEAN;
744 BEGIN
745 IF TG_OP = 'DELETE' THEN
746 "reference_lost" := TRUE;
747 ELSE
748 "reference_lost" := NEW."initiative_id" != OLD."initiative_id";
749 END IF;
750 IF
751 "reference_lost" AND NOT EXISTS (
752 SELECT NULL FROM "draft" WHERE "initiative_id" = OLD."initiative_id"
753 )
754 THEN
755 DELETE FROM "initiative" WHERE "id" = OLD."initiative_id";
756 END IF;
757 RETURN NULL;
758 END;
759 $$;
761 CREATE CONSTRAINT TRIGGER "last_draft_deletes_initiative"
762 AFTER UPDATE OR DELETE ON "draft" DEFERRABLE INITIALLY DEFERRED
763 FOR EACH ROW EXECUTE PROCEDURE
764 "last_draft_deletes_initiative_trigger"();
766 COMMENT ON FUNCTION "last_draft_deletes_initiative_trigger"() IS 'Implementation of trigger "last_draft_deletes_initiative" on table "draft"';
767 COMMENT ON TRIGGER "last_draft_deletes_initiative" ON "draft" IS 'Removing the last draft of an initiative deletes the initiative';
770 CREATE FUNCTION "suggestion_requires_first_opinion_trigger"()
771 RETURNS TRIGGER
772 LANGUAGE 'plpgsql' VOLATILE AS $$
773 BEGIN
774 IF NOT EXISTS (
775 SELECT NULL FROM "opinion" WHERE "suggestion_id" = NEW."id"
776 ) THEN
777 RAISE EXCEPTION 'Cannot create a suggestion without an opinion.';
778 END IF;
779 RETURN NULL;
780 END;
781 $$;
783 CREATE CONSTRAINT TRIGGER "suggestion_requires_first_opinion"
784 AFTER INSERT OR UPDATE ON "suggestion" DEFERRABLE INITIALLY DEFERRED
785 FOR EACH ROW EXECUTE PROCEDURE
786 "suggestion_requires_first_opinion_trigger"();
788 COMMENT ON FUNCTION "suggestion_requires_first_opinion_trigger"() IS 'Implementation of trigger "suggestion_requires_first_opinion" on table "suggestion"';
789 COMMENT ON TRIGGER "suggestion_requires_first_opinion" ON "suggestion" IS 'Ensure that new suggestions have at least one opinion';
792 CREATE FUNCTION "last_opinion_deletes_suggestion_trigger"()
793 RETURNS TRIGGER
794 LANGUAGE 'plpgsql' VOLATILE AS $$
795 DECLARE
796 "reference_lost" BOOLEAN;
797 BEGIN
798 IF TG_OP = 'DELETE' THEN
799 "reference_lost" := TRUE;
800 ELSE
801 "reference_lost" := NEW."suggestion_id" != OLD."suggestion_id";
802 END IF;
803 IF
804 "reference_lost" AND NOT EXISTS (
805 SELECT NULL FROM "opinion" WHERE "suggestion_id" = OLD."suggestion_id"
806 )
807 THEN
808 DELETE FROM "suggestion" WHERE "id" = OLD."suggestion_id";
809 END IF;
810 RETURN NULL;
811 END;
812 $$;
814 CREATE CONSTRAINT TRIGGER "last_opinion_deletes_suggestion"
815 AFTER UPDATE OR DELETE ON "opinion" DEFERRABLE INITIALLY DEFERRED
816 FOR EACH ROW EXECUTE PROCEDURE
817 "last_opinion_deletes_suggestion_trigger"();
819 COMMENT ON FUNCTION "last_opinion_deletes_suggestion_trigger"() IS 'Implementation of trigger "last_opinion_deletes_suggestion" on table "opinion"';
820 COMMENT ON TRIGGER "last_opinion_deletes_suggestion" ON "opinion" IS 'Removing the last opinion of a suggestion deletes the suggestion';
824 --------------------------------------------------------------------
825 -- Auto-retrieval of fields only needed for referential integrity --
826 --------------------------------------------------------------------
828 CREATE FUNCTION "autofill_issue_id_trigger"()
829 RETURNS TRIGGER
830 LANGUAGE 'plpgsql' VOLATILE AS $$
831 BEGIN
832 IF NEW."issue_id" ISNULL THEN
833 SELECT "issue_id" INTO NEW."issue_id"
834 FROM "initiative" WHERE "id" = NEW."initiative_id";
835 END IF;
836 RETURN NEW;
837 END;
838 $$;
840 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "supporter"
841 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
843 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "vote"
844 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
846 COMMENT ON FUNCTION "autofill_issue_id_trigger"() IS 'Implementation of triggers "autofill_issue_id" on tables "supporter" and "vote"';
847 COMMENT ON TRIGGER "autofill_issue_id" ON "supporter" IS 'Set "issue_id" field automatically, if NULL';
848 COMMENT ON TRIGGER "autofill_issue_id" ON "vote" IS 'Set "issue_id" field automatically, if NULL';
851 CREATE FUNCTION "autofill_initiative_id_trigger"()
852 RETURNS TRIGGER
853 LANGUAGE 'plpgsql' VOLATILE AS $$
854 BEGIN
855 IF NEW."initiative_id" ISNULL THEN
856 SELECT "initiative_id" INTO NEW."initiative_id"
857 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
858 END IF;
859 RETURN NEW;
860 END;
861 $$;
863 CREATE TRIGGER "autofill_initiative_id" BEFORE INSERT ON "opinion"
864 FOR EACH ROW EXECUTE PROCEDURE "autofill_initiative_id_trigger"();
866 COMMENT ON FUNCTION "autofill_initiative_id_trigger"() IS 'Implementation of trigger "autofill_initiative_id" on table "opinion"';
867 COMMENT ON TRIGGER "autofill_initiative_id" ON "opinion" IS 'Set "initiative_id" field automatically, if NULL';
871 -----------------------------------------------------
872 -- Automatic calculation of certain default values --
873 -----------------------------------------------------
875 CREATE FUNCTION "copy_autoreject_trigger"()
876 RETURNS TRIGGER
877 LANGUAGE 'plpgsql' VOLATILE AS $$
878 BEGIN
879 IF NEW."autoreject" ISNULL THEN
880 SELECT "membership"."autoreject" INTO NEW."autoreject"
881 FROM "issue" JOIN "membership"
882 ON "issue"."area_id" = "membership"."area_id"
883 WHERE "issue"."id" = NEW."issue_id"
884 AND "membership"."member_id" = NEW."member_id";
885 END IF;
886 IF NEW."autoreject" ISNULL THEN
887 NEW."autoreject" := FALSE;
888 END IF;
889 RETURN NEW;
890 END;
891 $$;
893 CREATE TRIGGER "copy_autoreject" BEFORE INSERT OR UPDATE ON "interest"
894 FOR EACH ROW EXECUTE PROCEDURE "copy_autoreject_trigger"();
896 COMMENT ON FUNCTION "copy_autoreject_trigger"() IS 'Implementation of trigger "copy_autoreject" on table "interest"';
897 COMMENT ON TRIGGER "copy_autoreject" ON "interest" IS 'If "autoreject" is NULL, then copy it from the area setting, or set to FALSE, if no membership existent';
900 CREATE FUNCTION "supporter_default_for_draft_id_trigger"()
901 RETURNS TRIGGER
902 LANGUAGE 'plpgsql' VOLATILE AS $$
903 BEGIN
904 IF NEW."draft_id" ISNULL THEN
905 SELECT "id" INTO NEW."draft_id" FROM "current_draft"
906 WHERE "initiative_id" = NEW."initiative_id";
907 END IF;
908 RETURN NEW;
909 END;
910 $$;
912 CREATE TRIGGER "default_for_draft_id" BEFORE INSERT OR UPDATE ON "supporter"
913 FOR EACH ROW EXECUTE PROCEDURE "supporter_default_for_draft_id_trigger"();
915 COMMENT ON FUNCTION "supporter_default_for_draft_id_trigger"() IS 'Implementation of trigger "default_for_draft" on table "supporter"';
916 COMMENT ON TRIGGER "default_for_draft_id" ON "supporter" IS 'If "draft_id" is NULL, then use the current draft of the initiative as default';
920 ----------------------------------------
921 -- Automatic creation of dependencies --
922 ----------------------------------------
924 CREATE FUNCTION "autocreate_interest_trigger"()
925 RETURNS TRIGGER
926 LANGUAGE 'plpgsql' VOLATILE AS $$
927 BEGIN
928 IF NOT EXISTS (
929 SELECT NULL FROM "initiative" JOIN "interest"
930 ON "initiative"."issue_id" = "interest"."issue_id"
931 WHERE "initiative"."id" = NEW."initiative_id"
932 AND "interest"."member_id" = NEW."member_id"
933 ) THEN
934 BEGIN
935 INSERT INTO "interest" ("issue_id", "member_id")
936 SELECT "issue_id", NEW."member_id"
937 FROM "initiative" WHERE "id" = NEW."initiative_id";
938 EXCEPTION WHEN unique_violation THEN END;
939 END IF;
940 RETURN NEW;
941 END;
942 $$;
944 CREATE TRIGGER "autocreate_interest" BEFORE INSERT ON "supporter"
945 FOR EACH ROW EXECUTE PROCEDURE "autocreate_interest_trigger"();
947 COMMENT ON FUNCTION "autocreate_interest_trigger"() IS 'Implementation of trigger "autocreate_interest" on table "supporter"';
948 COMMENT ON TRIGGER "autocreate_interest" ON "supporter" IS 'Supporting an initiative implies interest in the issue, thus automatically creates an entry in the "interest" table';
951 CREATE FUNCTION "autocreate_supporter_trigger"()
952 RETURNS TRIGGER
953 LANGUAGE 'plpgsql' VOLATILE AS $$
954 BEGIN
955 IF NOT EXISTS (
956 SELECT NULL FROM "suggestion" JOIN "supporter"
957 ON "suggestion"."initiative_id" = "supporter"."initiative_id"
958 WHERE "suggestion"."id" = NEW."suggestion_id"
959 AND "supporter"."member_id" = NEW."member_id"
960 ) THEN
961 BEGIN
962 INSERT INTO "supporter" ("initiative_id", "member_id")
963 SELECT "initiative_id", NEW."member_id"
964 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
965 EXCEPTION WHEN unique_violation THEN END;
966 END IF;
967 RETURN NEW;
968 END;
969 $$;
971 CREATE TRIGGER "autocreate_supporter" BEFORE INSERT ON "opinion"
972 FOR EACH ROW EXECUTE PROCEDURE "autocreate_supporter_trigger"();
974 COMMENT ON FUNCTION "autocreate_supporter_trigger"() IS 'Implementation of trigger "autocreate_supporter" on table "opinion"';
975 COMMENT ON TRIGGER "autocreate_supporter" ON "opinion" IS 'Opinions can only be added for supported initiatives. This trigger automatrically creates an entry in the "supporter" table, if not existent yet.';
979 ------------------------------------------
980 -- Views and helper functions for views --
981 ------------------------------------------
984 CREATE TYPE "delegation_scope" AS ENUM
985 ('global', 'area', 'issue');
987 COMMENT ON TYPE "delegation_scope" IS 'Scope for delegations: ''global'', ''area'', or ''issue''';
990 CREATE VIEW "global_delegation" AS
991 SELECT
992 "delegation"."id",
993 "delegation"."truster_id",
994 "delegation"."trustee_id"
995 FROM "delegation" JOIN "member"
996 ON "delegation"."trustee_id" = "member"."id"
997 WHERE "delegation"."area_id" ISNULL
998 AND "delegation"."issue_id" ISNULL
999 AND "member"."active";
1001 COMMENT ON VIEW "global_delegation" IS 'Global delegations to active members';
1004 CREATE VIEW "area_delegation" AS
1005 SELECT "subquery".* FROM (
1006 SELECT DISTINCT ON ("area"."id", "delegation"."truster_id")
1007 "area"."id" AS "area_id",
1008 "delegation"."id" AS "id",
1009 "delegation"."truster_id" AS "truster_id",
1010 "delegation"."trustee_id" AS "trustee_id",
1011 CASE WHEN "delegation"."area_id" ISNULL THEN
1012 'global'::"delegation_scope"
1013 ELSE
1014 'area'::"delegation_scope"
1015 END AS "scope"
1016 FROM "area" JOIN "delegation"
1017 ON ("delegation"."area_id" ISNULL OR "delegation"."area_id" = "area"."id")
1018 AND "delegation"."issue_id" ISNULL
1019 ORDER BY
1020 "area"."id",
1021 "delegation"."truster_id",
1022 "delegation"."area_id" NULLS LAST
1023 ) AS "subquery"
1024 JOIN "member" ON "subquery"."trustee_id" = "member"."id"
1025 WHERE "member"."active";
1027 COMMENT ON VIEW "area_delegation" IS 'Active delegations for areas';
1030 CREATE VIEW "issue_delegation" AS
1031 SELECT "subquery".* FROM (
1032 SELECT DISTINCT ON ("issue"."id", "delegation"."truster_id")
1033 "issue"."id" AS "issue_id",
1034 "delegation"."id" AS "id",
1035 "delegation"."truster_id" AS "truster_id",
1036 "delegation"."trustee_id" AS "trustee_id",
1037 CASE
1038 WHEN
1039 "delegation"."area_id" ISNULL AND
1040 "delegation"."issue_id" ISNULL
1041 THEN 'global'::"delegation_scope"
1042 WHEN
1043 "delegation"."area_id" NOTNULL
1044 THEN 'area'::"delegation_scope"
1045 ELSE 'issue'::"delegation_scope"
1046 END AS "scope"
1047 FROM "issue" JOIN "delegation"
1048 ON (
1049 "delegation"."area_id" ISNULL OR
1050 "delegation"."area_id" = "issue"."area_id"
1051 ) AND (
1052 "delegation"."issue_id" ISNULL OR
1053 "delegation"."issue_id" = "issue"."id"
1055 ORDER BY
1056 "issue"."id",
1057 "delegation"."truster_id",
1058 "delegation"."issue_id" NULLS LAST,
1059 "delegation"."area_id" NULLS LAST
1060 ) AS "subquery"
1061 JOIN "member" ON "subquery"."trustee_id" = "member"."id"
1062 WHERE "member"."active";
1064 COMMENT ON VIEW "issue_delegation" IS 'Active delegations for issues';
1067 CREATE FUNCTION "membership_weight_with_skipping"
1068 ( "area_id_p" "area"."id"%TYPE,
1069 "member_id_p" "member"."id"%TYPE,
1070 "skip_member_ids_p" INT4[] ) -- "member"."id"%TYPE[]
1071 RETURNS INT4
1072 LANGUAGE 'plpgsql' STABLE AS $$
1073 DECLARE
1074 "sum_v" INT4;
1075 "delegation_row" "area_delegation"%ROWTYPE;
1076 BEGIN
1077 "sum_v" := 1;
1078 FOR "delegation_row" IN
1079 SELECT "area_delegation".*
1080 FROM "area_delegation" LEFT JOIN "membership"
1081 ON "membership"."area_id" = "area_id_p"
1082 AND "membership"."member_id" = "area_delegation"."truster_id"
1083 WHERE "area_delegation"."area_id" = "area_id_p"
1084 AND "area_delegation"."trustee_id" = "member_id_p"
1085 AND "membership"."member_id" ISNULL
1086 LOOP
1087 IF NOT
1088 "skip_member_ids_p" @> ARRAY["delegation_row"."truster_id"]
1089 THEN
1090 "sum_v" := "sum_v" + "membership_weight_with_skipping"(
1091 "area_id_p",
1092 "delegation_row"."truster_id",
1093 "skip_member_ids_p" || "delegation_row"."truster_id"
1094 );
1095 END IF;
1096 END LOOP;
1097 RETURN "sum_v";
1098 END;
1099 $$;
1101 COMMENT ON FUNCTION "membership_weight_with_skipping"
1102 ( "area"."id"%TYPE,
1103 "member"."id"%TYPE,
1104 INT4[] )
1105 IS 'Helper function for "membership_weight" function';
1108 CREATE FUNCTION "membership_weight"
1109 ( "area_id_p" "area"."id"%TYPE,
1110 "member_id_p" "member"."id"%TYPE ) -- "member"."id"%TYPE[]
1111 RETURNS INT4
1112 LANGUAGE 'plpgsql' STABLE AS $$
1113 BEGIN
1114 RETURN "membership_weight_with_skipping"(
1115 "area_id_p",
1116 "member_id_p",
1117 ARRAY["member_id_p"]
1118 );
1119 END;
1120 $$;
1122 COMMENT ON FUNCTION "membership_weight"
1123 ( "area"."id"%TYPE,
1124 "member"."id"%TYPE )
1125 IS 'Calculates the potential voting weight of a member in a given area';
1128 CREATE VIEW "member_count_view" AS
1129 SELECT count(1) AS "total_count" FROM "member" WHERE "active";
1131 COMMENT ON VIEW "member_count_view" IS 'View used to update "member_count" table';
1134 CREATE VIEW "area_member_count" AS
1135 SELECT
1136 "area"."id" AS "area_id",
1137 count("member"."id") AS "direct_member_count",
1138 coalesce(
1139 sum(
1140 CASE WHEN "member"."id" NOTNULL THEN
1141 "membership_weight"("area"."id", "member"."id")
1142 ELSE 0 END
1144 ) AS "member_weight",
1145 coalesce(
1146 sum(
1147 CASE WHEN "member"."id" NOTNULL AND "membership"."autoreject" THEN
1148 "membership_weight"("area"."id", "member"."id")
1149 ELSE 0 END
1151 ) AS "autoreject_weight"
1152 FROM "area"
1153 LEFT JOIN "membership"
1154 ON "area"."id" = "membership"."area_id"
1155 LEFT JOIN "member"
1156 ON "membership"."member_id" = "member"."id"
1157 AND "member"."active"
1158 GROUP BY "area"."id";
1160 COMMENT ON VIEW "area_member_count" IS 'View used to update "member_count" column of table "area"';
1163 CREATE VIEW "opening_draft" AS
1164 SELECT "draft".* FROM (
1165 SELECT
1166 "initiative"."id" AS "initiative_id",
1167 min("draft"."id") AS "draft_id"
1168 FROM "initiative" JOIN "draft"
1169 ON "initiative"."id" = "draft"."initiative_id"
1170 GROUP BY "initiative"."id"
1171 ) AS "subquery"
1172 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
1174 COMMENT ON VIEW "opening_draft" IS 'First drafts of all initiatives';
1177 CREATE VIEW "current_draft" AS
1178 SELECT "draft".* FROM (
1179 SELECT
1180 "initiative"."id" AS "initiative_id",
1181 max("draft"."id") AS "draft_id"
1182 FROM "initiative" JOIN "draft"
1183 ON "initiative"."id" = "draft"."initiative_id"
1184 GROUP BY "initiative"."id"
1185 ) AS "subquery"
1186 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
1188 COMMENT ON VIEW "current_draft" IS 'All latest drafts for each initiative';
1191 CREATE VIEW "critical_opinion" AS
1192 SELECT * FROM "opinion"
1193 WHERE ("degree" = 2 AND "fulfilled" = FALSE)
1194 OR ("degree" = -2 AND "fulfilled" = TRUE);
1196 COMMENT ON VIEW "critical_opinion" IS 'Opinions currently causing dissatisfaction';
1199 CREATE VIEW "battle_participant" AS
1200 SELECT "issue_id", "id" AS "initiative_id" FROM "initiative"
1201 WHERE "admitted"
1202 AND "positive_votes" > "negative_votes";
1204 COMMENT ON VIEW "battle_participant" IS 'Helper view for "battle" view';
1207 CREATE VIEW "battle" AS
1208 SELECT
1209 "issue"."id" AS "issue_id",
1210 "winning_initiative"."initiative_id" AS "winning_initiative_id",
1211 "losing_initiative"."initiative_id" AS "losing_initiative_id",
1212 sum(
1213 CASE WHEN
1214 coalesce("better_vote"."grade", 0) >
1215 coalesce("worse_vote"."grade", 0)
1216 THEN "direct_voter"."weight" ELSE 0 END
1217 ) AS "count"
1218 FROM "issue"
1219 LEFT JOIN "direct_voter"
1220 ON "issue"."id" = "direct_voter"."issue_id"
1221 JOIN "battle_participant" AS "winning_initiative"
1222 ON "issue"."id" = "winning_initiative"."issue_id"
1223 JOIN "battle_participant" AS "losing_initiative"
1224 ON "issue"."id" = "losing_initiative"."issue_id"
1225 LEFT JOIN "vote" AS "better_vote"
1226 ON "direct_voter"."member_id" = "better_vote"."member_id"
1227 AND "winning_initiative"."initiative_id" = "better_vote"."initiative_id"
1228 LEFT JOIN "vote" AS "worse_vote"
1229 ON "direct_voter"."member_id" = "worse_vote"."member_id"
1230 AND "losing_initiative"."initiative_id" = "worse_vote"."initiative_id"
1231 WHERE
1232 "winning_initiative"."initiative_id" !=
1233 "losing_initiative"."initiative_id"
1234 GROUP BY
1235 "issue"."id",
1236 "winning_initiative"."initiative_id",
1237 "losing_initiative"."initiative_id";
1239 COMMENT ON VIEW "battle" IS 'Number of members preferring one initiative over another';
1242 CREATE VIEW "expired_session" AS
1243 SELECT * FROM "session" WHERE now() > "expiry";
1245 CREATE RULE "delete" AS ON DELETE TO "expired_session" DO INSTEAD
1246 DELETE FROM "session" WHERE "ident" = OLD."ident";
1248 COMMENT ON VIEW "expired_session" IS 'View containing all expired sessions where DELETE is possible';
1249 COMMENT ON RULE "delete" ON "expired_session" IS 'Rule allowing DELETE on rows in "expired_session" view, i.e. DELETE FROM "expired_session"';
1252 CREATE VIEW "open_issue" AS
1253 SELECT * FROM "issue" WHERE "closed" ISNULL;
1255 COMMENT ON VIEW "open_issue" IS 'All open issues';
1258 CREATE VIEW "issue_with_ranks_missing" AS
1259 SELECT * FROM "issue"
1260 WHERE "fully_frozen" NOTNULL
1261 AND "closed" NOTNULL
1262 AND "ranks_available" = FALSE;
1264 COMMENT ON VIEW "issue_with_ranks_missing" IS 'Issues where voting was finished, but no ranks have been calculated yet';
1267 CREATE VIEW "member_contingent" AS
1268 SELECT
1269 "member"."id" AS "member_id",
1270 "contingent"."time_frame",
1271 CASE WHEN "contingent"."text_entry_limit" NOTNULL THEN
1273 SELECT count(1) FROM "draft"
1274 WHERE "draft"."author_id" = "member"."id"
1275 AND "draft"."created" > now() - "contingent"."time_frame"
1276 ) + (
1277 SELECT count(1) FROM "suggestion"
1278 WHERE "suggestion"."author_id" = "member"."id"
1279 AND "suggestion"."created" > now() - "contingent"."time_frame"
1281 ELSE NULL END AS "text_entry_count",
1282 "contingent"."text_entry_limit",
1283 CASE WHEN "contingent"."initiative_limit" NOTNULL THEN (
1284 SELECT count(1) FROM "opening_draft"
1285 WHERE "opening_draft"."author_id" = "member"."id"
1286 AND "opening_draft"."created" > now() - "contingent"."time_frame"
1287 ) ELSE NULL END AS "initiative_count",
1288 "contingent"."initiative_limit"
1289 FROM "member" CROSS JOIN "contingent";
1291 COMMENT ON VIEW "member_contingent" IS 'Actual counts of text entries and initiatives are calculated per member for each limit in the "contingent" table.';
1293 COMMENT ON COLUMN "member_contingent"."text_entry_count" IS 'Only calculated when "text_entry_limit" is not null in the same row';
1294 COMMENT ON COLUMN "member_contingent"."initiative_count" IS 'Only calculated when "initiative_limit" is not null in the same row';
1297 CREATE VIEW "member_contingent_left" AS
1298 SELECT
1299 "member_id",
1300 max("text_entry_limit" - "text_entry_count") AS "text_entries_left",
1301 max("initiative_limit" - "initiative_count") AS "initiatives_left"
1302 FROM "member_contingent" GROUP BY "member_id";
1304 COMMENT ON VIEW "member_contingent_left" IS 'Amount of text entries or initiatives which can be posted now instantly by a member. This view should be used by a frontend to determine, if the contingent for posting is exhausted.';
1308 --------------------------------------------------
1309 -- Set returning function for delegation chains --
1310 --------------------------------------------------
1313 CREATE TYPE "delegation_chain_loop_tag" AS ENUM
1314 ('first', 'intermediate', 'last', 'repetition');
1316 COMMENT ON TYPE "delegation_chain_loop_tag" IS 'Type for loop tags in "delegation_chain_row" type';
1319 CREATE TYPE "delegation_chain_row" AS (
1320 "index" INT4,
1321 "member_id" INT4,
1322 "member_active" BOOLEAN,
1323 "participation" BOOLEAN,
1324 "overridden" BOOLEAN,
1325 "scope_in" "delegation_scope",
1326 "scope_out" "delegation_scope",
1327 "loop" "delegation_chain_loop_tag" );
1329 COMMENT ON TYPE "delegation_chain_row" IS 'Type of rows returned by "delegation_chain"(...) functions';
1331 COMMENT ON COLUMN "delegation_chain_row"."index" IS 'Index starting with 0 and counting up';
1332 COMMENT ON COLUMN "delegation_chain_row"."participation" IS 'In case of delegation chains for issues: interest, for areas: membership, for global delegation chains: always null';
1333 COMMENT ON COLUMN "delegation_chain_row"."overridden" IS 'True, if an entry with lower index has "participation" set to true';
1334 COMMENT ON COLUMN "delegation_chain_row"."scope_in" IS 'Scope of used incoming delegation';
1335 COMMENT ON COLUMN "delegation_chain_row"."scope_out" IS 'Scope of used outgoing delegation';
1336 COMMENT ON COLUMN "delegation_chain_row"."loop" IS 'Not null, if member is part of a loop, see "delegation_chain_loop_tag" type';
1339 CREATE FUNCTION "delegation_chain"
1340 ( "member_id_p" "member"."id"%TYPE,
1341 "area_id_p" "area"."id"%TYPE,
1342 "issue_id_p" "issue"."id"%TYPE,
1343 "simulate_trustee_id_p" "member"."id"%TYPE )
1344 RETURNS SETOF "delegation_chain_row"
1345 LANGUAGE 'plpgsql' STABLE AS $$
1346 DECLARE
1347 "issue_row" "issue"%ROWTYPE;
1348 "visited_member_ids" INT4[]; -- "member"."id"%TYPE[]
1349 "loop_member_id_v" "member"."id"%TYPE;
1350 "output_row" "delegation_chain_row";
1351 "output_rows" "delegation_chain_row"[];
1352 "delegation_row" "delegation"%ROWTYPE;
1353 "row_count" INT4;
1354 "i" INT4;
1355 "loop_v" BOOLEAN;
1356 BEGIN
1357 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
1358 "visited_member_ids" := '{}';
1359 "loop_member_id_v" := NULL;
1360 "output_rows" := '{}';
1361 "output_row"."index" := 0;
1362 "output_row"."member_id" := "member_id_p";
1363 "output_row"."member_active" := TRUE;
1364 "output_row"."participation" := FALSE;
1365 "output_row"."overridden" := FALSE;
1366 "output_row"."scope_out" := NULL;
1367 LOOP
1368 IF "visited_member_ids" @> ARRAY["output_row"."member_id"] THEN
1369 "loop_member_id_v" := "output_row"."member_id";
1370 ELSE
1371 "visited_member_ids" :=
1372 "visited_member_ids" || "output_row"."member_id";
1373 END IF;
1374 IF "output_row"."participation" THEN
1375 "output_row"."overridden" := TRUE;
1376 END IF;
1377 "output_row"."scope_in" := "output_row"."scope_out";
1378 IF EXISTS (
1379 SELECT NULL FROM "member"
1380 WHERE "id" = "output_row"."member_id" AND "active"
1381 ) THEN
1382 IF "area_id_p" ISNULL AND "issue_id_p" ISNULL THEN
1383 SELECT * INTO "delegation_row" FROM "delegation"
1384 WHERE "truster_id" = "output_row"."member_id"
1385 AND "area_id" ISNULL AND "issue_id" ISNULL;
1386 ELSIF "area_id_p" NOTNULL AND "issue_id_p" ISNULL THEN
1387 "output_row"."participation" := EXISTS (
1388 SELECT NULL FROM "membership"
1389 WHERE "area_id" = "area_id_p"
1390 AND "member_id" = "output_row"."member_id"
1391 );
1392 SELECT * INTO "delegation_row" FROM "delegation"
1393 WHERE "truster_id" = "output_row"."member_id"
1394 AND ("area_id" ISNULL OR "area_id" = "area_id_p")
1395 AND "issue_id" ISNULL
1396 ORDER BY "area_id" NULLS LAST;
1397 ELSIF "area_id_p" ISNULL AND "issue_id_p" NOTNULL THEN
1398 "output_row"."participation" := EXISTS (
1399 SELECT NULL FROM "interest"
1400 WHERE "issue_id" = "issue_id_p"
1401 AND "member_id" = "output_row"."member_id"
1402 );
1403 SELECT * INTO "delegation_row" FROM "delegation"
1404 WHERE "truster_id" = "output_row"."member_id"
1405 AND ("area_id" ISNULL OR "area_id" = "issue_row"."area_id")
1406 AND ("issue_id" ISNULL OR "issue_id" = "issue_id_p")
1407 ORDER BY "issue_id" NULLS LAST, "area_id" NULLS LAST;
1408 ELSE
1409 RAISE EXCEPTION 'Either area_id or issue_id or both must be NULL.';
1410 END IF;
1411 ELSE
1412 "output_row"."member_active" := FALSE;
1413 "output_row"."participation" := FALSE;
1414 "output_row"."scope_out" := NULL;
1415 "delegation_row" := ROW(NULL);
1416 END IF;
1417 IF
1418 "output_row"."member_id" = "member_id_p" AND
1419 "simulate_trustee_id_p" NOTNULL
1420 THEN
1421 "output_row"."scope_out" := CASE
1422 WHEN "area_id_p" ISNULL AND "issue_id_p" ISNULL THEN 'global'
1423 WHEN "area_id_p" NOTNULL AND "issue_id_p" ISNULL THEN 'area'
1424 WHEN "area_id_p" ISNULL AND "issue_id_p" NOTNULL THEN 'issue'
1425 END;
1426 "output_rows" := "output_rows" || "output_row";
1427 "output_row"."member_id" := "simulate_trustee_id_p";
1428 ELSIF "delegation_row"."trustee_id" NOTNULL THEN
1429 "output_row"."scope_out" := CASE
1430 WHEN
1431 "delegation_row"."area_id" ISNULL AND
1432 "delegation_row"."issue_id" ISNULL
1433 THEN 'global'
1434 WHEN
1435 "delegation_row"."area_id" NOTNULL AND
1436 "delegation_row"."issue_id" ISNULL
1437 THEN 'area'
1438 WHEN
1439 "delegation_row"."area_id" ISNULL AND
1440 "delegation_row"."issue_id" NOTNULL
1441 THEN 'issue'
1442 END;
1443 "output_rows" := "output_rows" || "output_row";
1444 "output_row"."member_id" := "delegation_row"."trustee_id";
1445 ELSE
1446 "output_row"."scope_out" := NULL;
1447 "output_rows" := "output_rows" || "output_row";
1448 EXIT;
1449 END IF;
1450 EXIT WHEN "loop_member_id_v" NOTNULL;
1451 "output_row"."index" := "output_row"."index" + 1;
1452 END LOOP;
1453 "row_count" := array_upper("output_rows", 1);
1454 "i" := 1;
1455 "loop_v" := FALSE;
1456 LOOP
1457 "output_row" := "output_rows"["i"];
1458 EXIT WHEN "output_row"."member_id" ISNULL;
1459 IF "loop_v" THEN
1460 IF "i" + 1 = "row_count" THEN
1461 "output_row"."loop" := 'last';
1462 ELSIF "i" = "row_count" THEN
1463 "output_row"."loop" := 'repetition';
1464 ELSE
1465 "output_row"."loop" := 'intermediate';
1466 END IF;
1467 ELSIF "output_row"."member_id" = "loop_member_id_v" THEN
1468 "output_row"."loop" := 'first';
1469 "loop_v" := TRUE;
1470 END IF;
1471 IF "area_id_p" ISNULL AND "issue_id_p" ISNULL THEN
1472 "output_row"."participation" := NULL;
1473 END IF;
1474 RETURN NEXT "output_row";
1475 "i" := "i" + 1;
1476 END LOOP;
1477 RETURN;
1478 END;
1479 $$;
1481 COMMENT ON FUNCTION "delegation_chain"
1482 ( "member"."id"%TYPE,
1483 "area"."id"%TYPE,
1484 "issue"."id"%TYPE,
1485 "member"."id"%TYPE )
1486 IS 'Helper function for frontends to display delegation chains; Not part of internal voting logic';
1488 CREATE FUNCTION "delegation_chain"
1489 ( "member_id_p" "member"."id"%TYPE,
1490 "area_id_p" "area"."id"%TYPE,
1491 "issue_id_p" "issue"."id"%TYPE )
1492 RETURNS SETOF "delegation_chain_row"
1493 LANGUAGE 'plpgsql' STABLE AS $$
1494 DECLARE
1495 "result_row" "delegation_chain_row";
1496 BEGIN
1497 FOR "result_row" IN
1498 SELECT * FROM "delegation_chain"(
1499 "member_id_p", "area_id_p", "issue_id_p", NULL
1501 LOOP
1502 RETURN NEXT "result_row";
1503 END LOOP;
1504 RETURN;
1505 END;
1506 $$;
1508 COMMENT ON FUNCTION "delegation_chain"
1509 ( "member"."id"%TYPE,
1510 "area"."id"%TYPE,
1511 "issue"."id"%TYPE )
1512 IS 'Shortcut for "delegation_chain"(...) function where 4th parameter is null';
1516 ------------------------------
1517 -- Comparison by vote count --
1518 ------------------------------
1520 CREATE FUNCTION "vote_ratio"
1521 ( "positive_votes_p" "initiative"."positive_votes"%TYPE,
1522 "negative_votes_p" "initiative"."negative_votes"%TYPE )
1523 RETURNS FLOAT8
1524 LANGUAGE 'plpgsql' STABLE AS $$
1525 DECLARE
1526 "total_v" INT4;
1527 BEGIN
1528 "total_v" := "positive_votes_p" + "negative_votes_p";
1529 IF "total_v" > 0 THEN
1530 RETURN "positive_votes_p"::FLOAT8 / "total_v"::FLOAT8;
1531 ELSE
1532 RETURN 0.5;
1533 END IF;
1534 END;
1535 $$;
1537 COMMENT ON FUNCTION "vote_ratio"
1538 ( "initiative"."positive_votes"%TYPE,
1539 "initiative"."negative_votes"%TYPE )
1540 IS 'Ratio of positive votes to sum of positive and negative votes; 0.5, if there are neither positive nor negative votes';
1544 ------------------------------------------------
1545 -- Locking for snapshots and voting procedure --
1546 ------------------------------------------------
1548 CREATE FUNCTION "global_lock"() RETURNS VOID
1549 LANGUAGE 'plpgsql' VOLATILE AS $$
1550 BEGIN
1551 -- NOTE: PostgreSQL allows reading, while tables are locked in
1552 -- exclusive move. Transactions should be kept short anyway!
1553 LOCK TABLE "member" IN EXCLUSIVE MODE;
1554 LOCK TABLE "area" IN EXCLUSIVE MODE;
1555 LOCK TABLE "membership" IN EXCLUSIVE MODE;
1556 -- NOTE: "member", "area" and "membership" are locked first to
1557 -- prevent deadlocks in combination with "calculate_member_counts"()
1558 LOCK TABLE "policy" IN EXCLUSIVE MODE;
1559 LOCK TABLE "issue" IN EXCLUSIVE MODE;
1560 LOCK TABLE "initiative" IN EXCLUSIVE MODE;
1561 LOCK TABLE "draft" IN EXCLUSIVE MODE;
1562 LOCK TABLE "suggestion" IN EXCLUSIVE MODE;
1563 LOCK TABLE "interest" IN EXCLUSIVE MODE;
1564 LOCK TABLE "initiator" IN EXCLUSIVE MODE;
1565 LOCK TABLE "supporter" IN EXCLUSIVE MODE;
1566 LOCK TABLE "opinion" IN EXCLUSIVE MODE;
1567 LOCK TABLE "delegation" IN EXCLUSIVE MODE;
1568 LOCK TABLE "direct_population_snapshot" IN EXCLUSIVE MODE;
1569 LOCK TABLE "delegating_population_snapshot" IN EXCLUSIVE MODE;
1570 LOCK TABLE "direct_interest_snapshot" IN EXCLUSIVE MODE;
1571 LOCK TABLE "delegating_interest_snapshot" IN EXCLUSIVE MODE;
1572 LOCK TABLE "direct_supporter_snapshot" IN EXCLUSIVE MODE;
1573 LOCK TABLE "direct_voter" IN EXCLUSIVE MODE;
1574 LOCK TABLE "delegating_voter" IN EXCLUSIVE MODE;
1575 LOCK TABLE "vote" IN EXCLUSIVE MODE;
1576 RETURN;
1577 END;
1578 $$;
1580 COMMENT ON FUNCTION "global_lock"() IS 'Locks all tables related to support/voting until end of transaction; read access is still possible though';
1584 -------------------------------
1585 -- Materialize member counts --
1586 -------------------------------
1588 CREATE FUNCTION "calculate_member_counts"()
1589 RETURNS VOID
1590 LANGUAGE 'plpgsql' VOLATILE AS $$
1591 BEGIN
1592 LOCK TABLE "member" IN EXCLUSIVE MODE;
1593 LOCK TABLE "area" IN EXCLUSIVE MODE;
1594 LOCK TABLE "membership" IN EXCLUSIVE MODE;
1595 DELETE FROM "member_count";
1596 INSERT INTO "member_count" ("total_count")
1597 SELECT "total_count" FROM "member_count_view";
1598 UPDATE "area" SET
1599 "direct_member_count" = "view"."direct_member_count",
1600 "member_weight" = "view"."member_weight",
1601 "autoreject_weight" = "view"."autoreject_weight"
1602 FROM "area_member_count" AS "view"
1603 WHERE "view"."area_id" = "area"."id";
1604 RETURN;
1605 END;
1606 $$;
1608 COMMENT ON FUNCTION "calculate_member_counts"() IS 'Updates "member_count" table and "member_count" column of table "area" by materializing data from views "member_count_view" and "area_member_count"';
1612 ------------------------------
1613 -- Calculation of snapshots --
1614 ------------------------------
1616 CREATE FUNCTION "weight_of_added_delegations_for_population_snapshot"
1617 ( "issue_id_p" "issue"."id"%TYPE,
1618 "member_id_p" "member"."id"%TYPE,
1619 "delegate_member_ids_p" "delegating_population_snapshot"."delegate_member_ids"%TYPE )
1620 RETURNS "direct_population_snapshot"."weight"%TYPE
1621 LANGUAGE 'plpgsql' VOLATILE AS $$
1622 DECLARE
1623 "issue_delegation_row" "issue_delegation"%ROWTYPE;
1624 "delegate_member_ids_v" "delegating_population_snapshot"."delegate_member_ids"%TYPE;
1625 "weight_v" INT4;
1626 "sub_weight_v" INT4;
1627 BEGIN
1628 "weight_v" := 0;
1629 FOR "issue_delegation_row" IN
1630 SELECT * FROM "issue_delegation"
1631 WHERE "trustee_id" = "member_id_p"
1632 AND "issue_id" = "issue_id_p"
1633 LOOP
1634 IF NOT EXISTS (
1635 SELECT NULL FROM "direct_population_snapshot"
1636 WHERE "issue_id" = "issue_id_p"
1637 AND "event" = 'periodic'
1638 AND "member_id" = "issue_delegation_row"."truster_id"
1639 ) AND NOT EXISTS (
1640 SELECT NULL FROM "delegating_population_snapshot"
1641 WHERE "issue_id" = "issue_id_p"
1642 AND "event" = 'periodic'
1643 AND "member_id" = "issue_delegation_row"."truster_id"
1644 ) THEN
1645 "delegate_member_ids_v" :=
1646 "member_id_p" || "delegate_member_ids_p";
1647 INSERT INTO "delegating_population_snapshot"
1648 ("issue_id", "event", "member_id", "delegate_member_ids")
1649 VALUES (
1650 "issue_id_p",
1651 'periodic',
1652 "issue_delegation_row"."truster_id",
1653 "delegate_member_ids_v"
1654 );
1655 "sub_weight_v" := 1 +
1656 "weight_of_added_delegations_for_population_snapshot"(
1657 "issue_id_p",
1658 "issue_delegation_row"."truster_id",
1659 "delegate_member_ids_v"
1660 );
1661 UPDATE "delegating_population_snapshot"
1662 SET "weight" = "sub_weight_v"
1663 WHERE "issue_id" = "issue_id_p"
1664 AND "event" = 'periodic'
1665 AND "member_id" = "issue_delegation_row"."truster_id";
1666 "weight_v" := "weight_v" + "sub_weight_v";
1667 END IF;
1668 END LOOP;
1669 RETURN "weight_v";
1670 END;
1671 $$;
1673 COMMENT ON FUNCTION "weight_of_added_delegations_for_population_snapshot"
1674 ( "issue"."id"%TYPE,
1675 "member"."id"%TYPE,
1676 "delegating_population_snapshot"."delegate_member_ids"%TYPE )
1677 IS 'Helper function for "create_population_snapshot" function';
1680 CREATE FUNCTION "create_population_snapshot"
1681 ( "issue_id_p" "issue"."id"%TYPE )
1682 RETURNS VOID
1683 LANGUAGE 'plpgsql' VOLATILE AS $$
1684 DECLARE
1685 "member_id_v" "member"."id"%TYPE;
1686 BEGIN
1687 DELETE FROM "direct_population_snapshot"
1688 WHERE "issue_id" = "issue_id_p"
1689 AND "event" = 'periodic';
1690 DELETE FROM "delegating_population_snapshot"
1691 WHERE "issue_id" = "issue_id_p"
1692 AND "event" = 'periodic';
1693 INSERT INTO "direct_population_snapshot"
1694 ("issue_id", "event", "member_id", "interest_exists")
1695 SELECT DISTINCT ON ("issue_id", "member_id")
1696 "issue_id_p" AS "issue_id",
1697 'periodic' AS "event",
1698 "subquery"."member_id",
1699 "subquery"."interest_exists"
1700 FROM (
1701 SELECT
1702 "member"."id" AS "member_id",
1703 FALSE AS "interest_exists"
1704 FROM "issue"
1705 JOIN "area" ON "issue"."area_id" = "area"."id"
1706 JOIN "membership" ON "area"."id" = "membership"."area_id"
1707 JOIN "member" ON "membership"."member_id" = "member"."id"
1708 WHERE "issue"."id" = "issue_id_p"
1709 AND "member"."active"
1710 UNION
1711 SELECT
1712 "member"."id" AS "member_id",
1713 TRUE AS "interest_exists"
1714 FROM "interest" JOIN "member"
1715 ON "interest"."member_id" = "member"."id"
1716 WHERE "interest"."issue_id" = "issue_id_p"
1717 AND "member"."active"
1718 ) AS "subquery"
1719 ORDER BY
1720 "issue_id_p",
1721 "subquery"."member_id",
1722 "subquery"."interest_exists" DESC;
1723 FOR "member_id_v" IN
1724 SELECT "member_id" FROM "direct_population_snapshot"
1725 WHERE "issue_id" = "issue_id_p"
1726 AND "event" = 'periodic'
1727 LOOP
1728 UPDATE "direct_population_snapshot" SET
1729 "weight" = 1 +
1730 "weight_of_added_delegations_for_population_snapshot"(
1731 "issue_id_p",
1732 "member_id_v",
1733 '{}'
1735 WHERE "issue_id" = "issue_id_p"
1736 AND "event" = 'periodic'
1737 AND "member_id" = "member_id_v";
1738 END LOOP;
1739 RETURN;
1740 END;
1741 $$;
1743 COMMENT ON FUNCTION "create_population_snapshot"
1744 ( "issue_id_p" "issue"."id"%TYPE )
1745 IS 'This function creates a new ''periodic'' population snapshot for the given issue. It does neither lock any tables, nor updates precalculated values in other tables.';
1748 CREATE FUNCTION "weight_of_added_delegations_for_interest_snapshot"
1749 ( "issue_id_p" "issue"."id"%TYPE,
1750 "member_id_p" "member"."id"%TYPE,
1751 "delegate_member_ids_p" "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
1752 RETURNS "direct_interest_snapshot"."weight"%TYPE
1753 LANGUAGE 'plpgsql' VOLATILE AS $$
1754 DECLARE
1755 "issue_delegation_row" "issue_delegation"%ROWTYPE;
1756 "delegate_member_ids_v" "delegating_interest_snapshot"."delegate_member_ids"%TYPE;
1757 "weight_v" INT4;
1758 "sub_weight_v" INT4;
1759 BEGIN
1760 "weight_v" := 0;
1761 FOR "issue_delegation_row" IN
1762 SELECT * FROM "issue_delegation"
1763 WHERE "trustee_id" = "member_id_p"
1764 AND "issue_id" = "issue_id_p"
1765 LOOP
1766 IF NOT EXISTS (
1767 SELECT NULL FROM "direct_interest_snapshot"
1768 WHERE "issue_id" = "issue_id_p"
1769 AND "event" = 'periodic'
1770 AND "member_id" = "issue_delegation_row"."truster_id"
1771 ) AND NOT EXISTS (
1772 SELECT NULL FROM "delegating_interest_snapshot"
1773 WHERE "issue_id" = "issue_id_p"
1774 AND "event" = 'periodic'
1775 AND "member_id" = "issue_delegation_row"."truster_id"
1776 ) THEN
1777 "delegate_member_ids_v" :=
1778 "member_id_p" || "delegate_member_ids_p";
1779 INSERT INTO "delegating_interest_snapshot"
1780 ("issue_id", "event", "member_id", "delegate_member_ids")
1781 VALUES (
1782 "issue_id_p",
1783 'periodic',
1784 "issue_delegation_row"."truster_id",
1785 "delegate_member_ids_v"
1786 );
1787 "sub_weight_v" := 1 +
1788 "weight_of_added_delegations_for_interest_snapshot"(
1789 "issue_id_p",
1790 "issue_delegation_row"."truster_id",
1791 "delegate_member_ids_v"
1792 );
1793 UPDATE "delegating_interest_snapshot"
1794 SET "weight" = "sub_weight_v"
1795 WHERE "issue_id" = "issue_id_p"
1796 AND "event" = 'periodic'
1797 AND "member_id" = "issue_delegation_row"."truster_id";
1798 "weight_v" := "weight_v" + "sub_weight_v";
1799 END IF;
1800 END LOOP;
1801 RETURN "weight_v";
1802 END;
1803 $$;
1805 COMMENT ON FUNCTION "weight_of_added_delegations_for_interest_snapshot"
1806 ( "issue"."id"%TYPE,
1807 "member"."id"%TYPE,
1808 "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
1809 IS 'Helper function for "create_interest_snapshot" function';
1812 CREATE FUNCTION "create_interest_snapshot"
1813 ( "issue_id_p" "issue"."id"%TYPE )
1814 RETURNS VOID
1815 LANGUAGE 'plpgsql' VOLATILE AS $$
1816 DECLARE
1817 "member_id_v" "member"."id"%TYPE;
1818 BEGIN
1819 DELETE FROM "direct_interest_snapshot"
1820 WHERE "issue_id" = "issue_id_p"
1821 AND "event" = 'periodic';
1822 DELETE FROM "delegating_interest_snapshot"
1823 WHERE "issue_id" = "issue_id_p"
1824 AND "event" = 'periodic';
1825 DELETE FROM "direct_supporter_snapshot"
1826 WHERE "issue_id" = "issue_id_p"
1827 AND "event" = 'periodic';
1828 INSERT INTO "direct_interest_snapshot"
1829 ("issue_id", "event", "member_id", "voting_requested")
1830 SELECT
1831 "issue_id_p" AS "issue_id",
1832 'periodic' AS "event",
1833 "member"."id" AS "member_id",
1834 "interest"."voting_requested"
1835 FROM "interest" JOIN "member"
1836 ON "interest"."member_id" = "member"."id"
1837 WHERE "interest"."issue_id" = "issue_id_p"
1838 AND "member"."active";
1839 FOR "member_id_v" IN
1840 SELECT "member_id" FROM "direct_interest_snapshot"
1841 WHERE "issue_id" = "issue_id_p"
1842 AND "event" = 'periodic'
1843 LOOP
1844 UPDATE "direct_interest_snapshot" SET
1845 "weight" = 1 +
1846 "weight_of_added_delegations_for_interest_snapshot"(
1847 "issue_id_p",
1848 "member_id_v",
1849 '{}'
1851 WHERE "issue_id" = "issue_id_p"
1852 AND "event" = 'periodic'
1853 AND "member_id" = "member_id_v";
1854 END LOOP;
1855 INSERT INTO "direct_supporter_snapshot"
1856 ( "issue_id", "initiative_id", "event", "member_id",
1857 "informed", "satisfied" )
1858 SELECT
1859 "issue_id_p" AS "issue_id",
1860 "initiative"."id" AS "initiative_id",
1861 'periodic' AS "event",
1862 "member"."id" AS "member_id",
1863 "supporter"."draft_id" = "current_draft"."id" AS "informed",
1864 NOT EXISTS (
1865 SELECT NULL FROM "critical_opinion"
1866 WHERE "initiative_id" = "initiative"."id"
1867 AND "member_id" = "member"."id"
1868 ) AS "satisfied"
1869 FROM "supporter"
1870 JOIN "member"
1871 ON "supporter"."member_id" = "member"."id"
1872 JOIN "initiative"
1873 ON "supporter"."initiative_id" = "initiative"."id"
1874 JOIN "current_draft"
1875 ON "initiative"."id" = "current_draft"."initiative_id"
1876 JOIN "direct_interest_snapshot"
1877 ON "member"."id" = "direct_interest_snapshot"."member_id"
1878 AND "initiative"."issue_id" = "direct_interest_snapshot"."issue_id"
1879 AND "event" = 'periodic'
1880 WHERE "member"."active"
1881 AND "initiative"."issue_id" = "issue_id_p";
1882 RETURN;
1883 END;
1884 $$;
1886 COMMENT ON FUNCTION "create_interest_snapshot"
1887 ( "issue"."id"%TYPE )
1888 IS 'This function creates a new ''periodic'' interest/supporter snapshot for the given issue. It does neither lock any tables, nor updates precalculated values in other tables.';
1891 CREATE FUNCTION "create_snapshot"
1892 ( "issue_id_p" "issue"."id"%TYPE )
1893 RETURNS VOID
1894 LANGUAGE 'plpgsql' VOLATILE AS $$
1895 DECLARE
1896 "initiative_id_v" "initiative"."id"%TYPE;
1897 "suggestion_id_v" "suggestion"."id"%TYPE;
1898 BEGIN
1899 PERFORM "global_lock"();
1900 PERFORM "create_population_snapshot"("issue_id_p");
1901 PERFORM "create_interest_snapshot"("issue_id_p");
1902 UPDATE "issue" SET
1903 "snapshot" = now(),
1904 "latest_snapshot_event" = 'periodic',
1905 "population" = (
1906 SELECT coalesce(sum("weight"), 0)
1907 FROM "direct_population_snapshot"
1908 WHERE "issue_id" = "issue_id_p"
1909 AND "event" = 'periodic'
1910 ),
1911 "vote_now" = (
1912 SELECT coalesce(sum("weight"), 0)
1913 FROM "direct_interest_snapshot"
1914 WHERE "issue_id" = "issue_id_p"
1915 AND "event" = 'periodic'
1916 AND "voting_requested" = TRUE
1917 ),
1918 "vote_later" = (
1919 SELECT coalesce(sum("weight"), 0)
1920 FROM "direct_interest_snapshot"
1921 WHERE "issue_id" = "issue_id_p"
1922 AND "event" = 'periodic'
1923 AND "voting_requested" = FALSE
1925 WHERE "id" = "issue_id_p";
1926 FOR "initiative_id_v" IN
1927 SELECT "id" FROM "initiative" WHERE "issue_id" = "issue_id_p"
1928 LOOP
1929 UPDATE "initiative" SET
1930 "supporter_count" = (
1931 SELECT coalesce(sum("di"."weight"), 0)
1932 FROM "direct_interest_snapshot" AS "di"
1933 JOIN "direct_supporter_snapshot" AS "ds"
1934 ON "di"."member_id" = "ds"."member_id"
1935 WHERE "di"."issue_id" = "issue_id_p"
1936 AND "di"."event" = 'periodic'
1937 AND "ds"."initiative_id" = "initiative_id_v"
1938 AND "ds"."event" = 'periodic'
1939 ),
1940 "informed_supporter_count" = (
1941 SELECT coalesce(sum("di"."weight"), 0)
1942 FROM "direct_interest_snapshot" AS "di"
1943 JOIN "direct_supporter_snapshot" AS "ds"
1944 ON "di"."member_id" = "ds"."member_id"
1945 WHERE "di"."issue_id" = "issue_id_p"
1946 AND "di"."event" = 'periodic'
1947 AND "ds"."initiative_id" = "initiative_id_v"
1948 AND "ds"."event" = 'periodic'
1949 AND "ds"."informed"
1950 ),
1951 "satisfied_supporter_count" = (
1952 SELECT coalesce(sum("di"."weight"), 0)
1953 FROM "direct_interest_snapshot" AS "di"
1954 JOIN "direct_supporter_snapshot" AS "ds"
1955 ON "di"."member_id" = "ds"."member_id"
1956 WHERE "di"."issue_id" = "issue_id_p"
1957 AND "di"."event" = 'periodic'
1958 AND "ds"."initiative_id" = "initiative_id_v"
1959 AND "ds"."event" = 'periodic'
1960 AND "ds"."satisfied"
1961 ),
1962 "satisfied_informed_supporter_count" = (
1963 SELECT coalesce(sum("di"."weight"), 0)
1964 FROM "direct_interest_snapshot" AS "di"
1965 JOIN "direct_supporter_snapshot" AS "ds"
1966 ON "di"."member_id" = "ds"."member_id"
1967 WHERE "di"."issue_id" = "issue_id_p"
1968 AND "di"."event" = 'periodic'
1969 AND "ds"."initiative_id" = "initiative_id_v"
1970 AND "ds"."event" = 'periodic'
1971 AND "ds"."informed"
1972 AND "ds"."satisfied"
1974 WHERE "id" = "initiative_id_v";
1975 FOR "suggestion_id_v" IN
1976 SELECT "id" FROM "suggestion"
1977 WHERE "initiative_id" = "initiative_id_v"
1978 LOOP
1979 UPDATE "suggestion" SET
1980 "minus2_unfulfilled_count" = (
1981 SELECT coalesce(sum("snapshot"."weight"), 0)
1982 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
1983 ON "opinion"."member_id" = "snapshot"."member_id"
1984 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
1985 AND "snapshot"."issue_id" = "issue_id_p"
1986 AND "opinion"."degree" = -2
1987 AND "opinion"."fulfilled" = FALSE
1988 ),
1989 "minus2_fulfilled_count" = (
1990 SELECT coalesce(sum("snapshot"."weight"), 0)
1991 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
1992 ON "opinion"."member_id" = "snapshot"."member_id"
1993 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
1994 AND "snapshot"."issue_id" = "issue_id_p"
1995 AND "opinion"."degree" = -2
1996 AND "opinion"."fulfilled" = TRUE
1997 ),
1998 "minus1_unfulfilled_count" = (
1999 SELECT coalesce(sum("snapshot"."weight"), 0)
2000 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
2001 ON "opinion"."member_id" = "snapshot"."member_id"
2002 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
2003 AND "snapshot"."issue_id" = "issue_id_p"
2004 AND "opinion"."degree" = -1
2005 AND "opinion"."fulfilled" = FALSE
2006 ),
2007 "minus1_fulfilled_count" = (
2008 SELECT coalesce(sum("snapshot"."weight"), 0)
2009 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
2010 ON "opinion"."member_id" = "snapshot"."member_id"
2011 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
2012 AND "snapshot"."issue_id" = "issue_id_p"
2013 AND "opinion"."degree" = -1
2014 AND "opinion"."fulfilled" = TRUE
2015 ),
2016 "plus1_unfulfilled_count" = (
2017 SELECT coalesce(sum("snapshot"."weight"), 0)
2018 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
2019 ON "opinion"."member_id" = "snapshot"."member_id"
2020 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
2021 AND "snapshot"."issue_id" = "issue_id_p"
2022 AND "opinion"."degree" = 1
2023 AND "opinion"."fulfilled" = FALSE
2024 ),
2025 "plus1_fulfilled_count" = (
2026 SELECT coalesce(sum("snapshot"."weight"), 0)
2027 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
2028 ON "opinion"."member_id" = "snapshot"."member_id"
2029 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
2030 AND "snapshot"."issue_id" = "issue_id_p"
2031 AND "opinion"."degree" = 1
2032 AND "opinion"."fulfilled" = TRUE
2033 ),
2034 "plus2_unfulfilled_count" = (
2035 SELECT coalesce(sum("snapshot"."weight"), 0)
2036 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
2037 ON "opinion"."member_id" = "snapshot"."member_id"
2038 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
2039 AND "snapshot"."issue_id" = "issue_id_p"
2040 AND "opinion"."degree" = 2
2041 AND "opinion"."fulfilled" = FALSE
2042 ),
2043 "plus2_fulfilled_count" = (
2044 SELECT coalesce(sum("snapshot"."weight"), 0)
2045 FROM "opinion" JOIN "direct_interest_snapshot" AS "snapshot"
2046 ON "opinion"."member_id" = "snapshot"."member_id"
2047 WHERE "opinion"."suggestion_id" = "suggestion_id_v"
2048 AND "snapshot"."issue_id" = "issue_id_p"
2049 AND "opinion"."degree" = 2
2050 AND "opinion"."fulfilled" = TRUE
2052 WHERE "suggestion"."id" = "suggestion_id_v";
2053 END LOOP;
2054 END LOOP;
2055 RETURN;
2056 END;
2057 $$;
2059 COMMENT ON FUNCTION "create_snapshot"
2060 ( "issue"."id"%TYPE )
2061 IS 'This function creates a complete new ''periodic'' snapshot of population, interest and support for the given issue. All involved tables are locked, and after completion precalculated values in the source tables are updated.';
2064 CREATE FUNCTION "set_snapshot_event"
2065 ( "issue_id_p" "issue"."id"%TYPE,
2066 "event_p" "snapshot_event" )
2067 RETURNS VOID
2068 LANGUAGE 'plpgsql' VOLATILE AS $$
2069 BEGIN
2070 UPDATE "issue" SET "latest_snapshot_event" = "event_p"
2071 WHERE "id" = "issue_id_p";
2072 UPDATE "direct_population_snapshot" SET "event" = "event_p"
2073 WHERE "issue_id" = "issue_id_p" AND "event" = 'periodic';
2074 UPDATE "delegating_population_snapshot" SET "event" = "event_p"
2075 WHERE "issue_id" = "issue_id_p" AND "event" = 'periodic';
2076 UPDATE "direct_interest_snapshot" SET "event" = "event_p"
2077 WHERE "issue_id" = "issue_id_p" AND "event" = 'periodic';
2078 UPDATE "delegating_interest_snapshot" SET "event" = "event_p"
2079 WHERE "issue_id" = "issue_id_p" AND "event" = 'periodic';
2080 UPDATE "direct_supporter_snapshot" SET "event" = "event_p"
2081 WHERE "issue_id" = "issue_id_p" AND "event" = 'periodic';
2082 RETURN;
2083 END;
2084 $$;
2086 COMMENT ON FUNCTION "set_snapshot_event"
2087 ( "issue"."id"%TYPE,
2088 "snapshot_event" )
2089 IS 'Change "event" attribute of the previous ''periodic'' snapshot';
2093 ---------------------
2094 -- Freezing issues --
2095 ---------------------
2097 CREATE FUNCTION "freeze_after_snapshot"
2098 ( "issue_id_p" "issue"."id"%TYPE )
2099 RETURNS VOID
2100 LANGUAGE 'plpgsql' VOLATILE AS $$
2101 DECLARE
2102 "issue_row" "issue"%ROWTYPE;
2103 "policy_row" "policy"%ROWTYPE;
2104 "initiative_row" "initiative"%ROWTYPE;
2105 BEGIN
2106 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
2107 SELECT * INTO "policy_row"
2108 FROM "policy" WHERE "id" = "issue_row"."policy_id";
2109 PERFORM "set_snapshot_event"("issue_id_p", 'start_of_voting');
2110 UPDATE "issue" SET
2111 "accepted" = coalesce("accepted", now()),
2112 "half_frozen" = coalesce("half_frozen", now()),
2113 "fully_frozen" = now()
2114 WHERE "id" = "issue_id_p";
2115 FOR "initiative_row" IN
2116 SELECT * FROM "initiative" WHERE "issue_id" = "issue_id_p"
2117 LOOP
2118 IF
2119 "initiative_row"."satisfied_supporter_count" > 0 AND
2120 "initiative_row"."satisfied_supporter_count" *
2121 "policy_row"."initiative_quorum_den" >=
2122 "issue_row"."population" * "policy_row"."initiative_quorum_num"
2123 THEN
2124 UPDATE "initiative" SET "admitted" = TRUE
2125 WHERE "id" = "initiative_row"."id";
2126 ELSE
2127 UPDATE "initiative" SET "admitted" = FALSE
2128 WHERE "id" = "initiative_row"."id";
2129 END IF;
2130 END LOOP;
2131 IF NOT EXISTS (
2132 SELECT NULL FROM "initiative"
2133 WHERE "issue_id" = "issue_id_p" AND "admitted" = TRUE
2134 ) THEN
2135 PERFORM "close_voting"("issue_id_p");
2136 END IF;
2137 RETURN;
2138 END;
2139 $$;
2141 COMMENT ON FUNCTION "freeze_after_snapshot"
2142 ( "issue"."id"%TYPE )
2143 IS 'This function freezes an issue (fully) and starts voting, but must only be called when "create_snapshot" was called in the same transaction.';
2146 CREATE FUNCTION "manual_freeze"("issue_id_p" "issue"."id"%TYPE)
2147 RETURNS VOID
2148 LANGUAGE 'plpgsql' VOLATILE AS $$
2149 DECLARE
2150 "issue_row" "issue"%ROWTYPE;
2151 BEGIN
2152 PERFORM "create_snapshot"("issue_id_p");
2153 PERFORM "freeze_after_snapshot"("issue_id_p");
2154 RETURN;
2155 END;
2156 $$;
2158 COMMENT ON FUNCTION "freeze_after_snapshot"
2159 ( "issue"."id"%TYPE )
2160 IS 'Freeze an issue manually (fully) and start voting';
2164 -----------------------
2165 -- Counting of votes --
2166 -----------------------
2169 CREATE FUNCTION "weight_of_added_vote_delegations"
2170 ( "issue_id_p" "issue"."id"%TYPE,
2171 "member_id_p" "member"."id"%TYPE,
2172 "delegate_member_ids_p" "delegating_voter"."delegate_member_ids"%TYPE )
2173 RETURNS "direct_voter"."weight"%TYPE
2174 LANGUAGE 'plpgsql' VOLATILE AS $$
2175 DECLARE
2176 "issue_delegation_row" "issue_delegation"%ROWTYPE;
2177 "delegate_member_ids_v" "delegating_voter"."delegate_member_ids"%TYPE;
2178 "weight_v" INT4;
2179 "sub_weight_v" INT4;
2180 BEGIN
2181 "weight_v" := 0;
2182 FOR "issue_delegation_row" IN
2183 SELECT * FROM "issue_delegation"
2184 WHERE "trustee_id" = "member_id_p"
2185 AND "issue_id" = "issue_id_p"
2186 LOOP
2187 IF NOT EXISTS (
2188 SELECT NULL FROM "direct_voter"
2189 WHERE "member_id" = "issue_delegation_row"."truster_id"
2190 AND "issue_id" = "issue_id_p"
2191 ) AND NOT EXISTS (
2192 SELECT NULL FROM "delegating_voter"
2193 WHERE "member_id" = "issue_delegation_row"."truster_id"
2194 AND "issue_id" = "issue_id_p"
2195 ) THEN
2196 "delegate_member_ids_v" :=
2197 "member_id_p" || "delegate_member_ids_p";
2198 INSERT INTO "delegating_voter"
2199 ("issue_id", "member_id", "delegate_member_ids")
2200 VALUES (
2201 "issue_id_p",
2202 "issue_delegation_row"."truster_id",
2203 "delegate_member_ids_v"
2204 );
2205 "sub_weight_v" := 1 +
2206 "weight_of_added_vote_delegations"(
2207 "issue_id_p",
2208 "issue_delegation_row"."truster_id",
2209 "delegate_member_ids_v"
2210 );
2211 UPDATE "delegating_voter"
2212 SET "weight" = "sub_weight_v"
2213 WHERE "issue_id" = "issue_id_p"
2214 AND "member_id" = "issue_delegation_row"."truster_id";
2215 "weight_v" := "weight_v" + "sub_weight_v";
2216 END IF;
2217 END LOOP;
2218 RETURN "weight_v";
2219 END;
2220 $$;
2222 COMMENT ON FUNCTION "weight_of_added_vote_delegations"
2223 ( "issue"."id"%TYPE,
2224 "member"."id"%TYPE,
2225 "delegating_voter"."delegate_member_ids"%TYPE )
2226 IS 'Helper function for "add_vote_delegations" function';
2229 CREATE FUNCTION "add_vote_delegations"
2230 ( "issue_id_p" "issue"."id"%TYPE )
2231 RETURNS VOID
2232 LANGUAGE 'plpgsql' VOLATILE AS $$
2233 DECLARE
2234 "member_id_v" "member"."id"%TYPE;
2235 BEGIN
2236 FOR "member_id_v" IN
2237 SELECT "member_id" FROM "direct_voter"
2238 WHERE "issue_id" = "issue_id_p"
2239 LOOP
2240 UPDATE "direct_voter" SET
2241 "weight" = "weight" + "weight_of_added_vote_delegations"(
2242 "issue_id_p",
2243 "member_id_v",
2244 '{}'
2246 WHERE "member_id" = "member_id_v"
2247 AND "issue_id" = "issue_id_p";
2248 END LOOP;
2249 RETURN;
2250 END;
2251 $$;
2253 COMMENT ON FUNCTION "add_vote_delegations"
2254 ( "issue_id_p" "issue"."id"%TYPE )
2255 IS 'Helper function for "close_voting" function';
2258 CREATE FUNCTION "close_voting"("issue_id_p" "issue"."id"%TYPE)
2259 RETURNS VOID
2260 LANGUAGE 'plpgsql' VOLATILE AS $$
2261 DECLARE
2262 "issue_row" "issue"%ROWTYPE;
2263 "member_id_v" "member"."id"%TYPE;
2264 BEGIN
2265 PERFORM "global_lock"();
2266 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
2267 DELETE FROM "delegating_voter"
2268 WHERE "issue_id" = "issue_id_p";
2269 DELETE FROM "direct_voter"
2270 WHERE "issue_id" = "issue_id_p"
2271 AND "autoreject" = TRUE;
2272 DELETE FROM "direct_voter" USING "member"
2273 WHERE "direct_voter"."member_id" = "member"."id"
2274 AND "direct_voter"."issue_id" = "issue_id_p"
2275 AND "member"."active" = FALSE;
2276 UPDATE "direct_voter" SET "weight" = 1
2277 WHERE "issue_id" = "issue_id_p";
2278 PERFORM "add_vote_delegations"("issue_id_p");
2279 FOR "member_id_v" IN
2280 SELECT "interest"."member_id"
2281 FROM "interest"
2282 LEFT JOIN "direct_voter"
2283 ON "interest"."member_id" = "direct_voter"."member_id"
2284 AND "interest"."issue_id" = "direct_voter"."issue_id"
2285 LEFT JOIN "delegating_voter"
2286 ON "interest"."member_id" = "delegating_voter"."member_id"
2287 AND "interest"."issue_id" = "delegating_voter"."issue_id"
2288 WHERE "interest"."issue_id" = "issue_id_p"
2289 AND "interest"."autoreject" = TRUE
2290 AND "direct_voter"."member_id" ISNULL
2291 AND "delegating_voter"."member_id" ISNULL
2292 UNION SELECT "membership"."member_id"
2293 FROM "membership"
2294 LEFT JOIN "interest"
2295 ON "membership"."member_id" = "interest"."member_id"
2296 AND "interest"."issue_id" = "issue_id_p"
2297 LEFT JOIN "direct_voter"
2298 ON "membership"."member_id" = "direct_voter"."member_id"
2299 AND "direct_voter"."issue_id" = "issue_id_p"
2300 LEFT JOIN "delegating_voter"
2301 ON "membership"."member_id" = "delegating_voter"."member_id"
2302 AND "delegating_voter"."issue_id" = "issue_id_p"
2303 WHERE "membership"."area_id" = "issue_row"."area_id"
2304 AND "membership"."autoreject" = TRUE
2305 AND "interest"."autoreject" ISNULL
2306 AND "direct_voter"."member_id" ISNULL
2307 AND "delegating_voter"."member_id" ISNULL
2308 LOOP
2309 INSERT INTO "direct_voter" ("member_id", "issue_id", "autoreject")
2310 VALUES ("member_id_v", "issue_id_p", TRUE);
2311 INSERT INTO "vote" (
2312 "member_id",
2313 "issue_id",
2314 "initiative_id",
2315 "grade"
2316 ) SELECT
2317 "member_id_v" AS "member_id",
2318 "issue_id_p" AS "issue_id",
2319 "id" AS "initiative_id",
2320 -1 AS "grade"
2321 FROM "initiative" WHERE "issue_id" = "issue_id_p";
2322 END LOOP;
2323 PERFORM "add_vote_delegations"("issue_id_p");
2324 UPDATE "issue" SET
2325 "voter_count" = (
2326 SELECT coalesce(sum("weight"), 0)
2327 FROM "direct_voter" WHERE "issue_id" = "issue_id_p"
2329 WHERE "id" = "issue_id_p";
2330 UPDATE "initiative" SET
2331 "positive_votes" = "subquery"."positive_votes",
2332 "negative_votes" = "subquery"."negative_votes"
2333 FROM (
2334 SELECT
2335 "initiative"."id" AS "initiative_id",
2336 coalesce(
2337 sum(
2338 CASE WHEN "grade" > 0 THEN "direct_voter"."weight" ELSE 0 END
2339 ),
2341 ) AS "positive_votes",
2342 coalesce(
2343 sum(
2344 CASE WHEN "grade" < 0 THEN "direct_voter"."weight" ELSE 0 END
2345 ),
2347 ) AS "negative_votes"
2348 FROM "initiative"
2349 LEFT JOIN "direct_voter"
2350 ON "direct_voter"."issue_id" = "initiative"."issue_id"
2351 LEFT JOIN "vote"
2352 ON "vote"."initiative_id" = "initiative"."id"
2353 AND "vote"."member_id" = "direct_voter"."member_id"
2354 WHERE "initiative"."issue_id" = "issue_id_p"
2355 GROUP BY "initiative"."id"
2356 ) AS "subquery"
2357 WHERE "initiative"."admitted"
2358 AND "initiative"."id" = "subquery"."initiative_id";
2359 UPDATE "issue" SET "closed" = now() WHERE "id" = "issue_id_p";
2360 END;
2361 $$;
2363 COMMENT ON FUNCTION "close_voting"
2364 ( "issue"."id"%TYPE )
2365 IS 'Closes the voting on an issue, and calculates positive and negative votes for each initiative; The ranking is not calculated yet, to keep the (locking) transaction short.';
2368 CREATE FUNCTION "init_array"("dim_p" INTEGER)
2369 RETURNS INT4[]
2370 LANGUAGE 'plpgsql' IMMUTABLE AS $$
2371 DECLARE
2372 "i" INTEGER;
2373 "ary_text_v" TEXT;
2374 BEGIN
2375 IF "dim_p" >= 1 THEN
2376 "ary_text_v" := '{NULL';
2377 "i" := "dim_p";
2378 LOOP
2379 "i" := "i" - 1;
2380 EXIT WHEN "i" = 0;
2381 "ary_text_v" := "ary_text_v" || ',NULL';
2382 END LOOP;
2383 "ary_text_v" := "ary_text_v" || '}';
2384 RETURN "ary_text_v"::INT4[][];
2385 ELSE
2386 RAISE EXCEPTION 'Dimension needs to be at least 1.';
2387 END IF;
2388 END;
2389 $$;
2391 COMMENT ON FUNCTION "init_array"(INTEGER) IS 'Needed for PostgreSQL < 8.4, due to missing "array_fill" function';
2394 CREATE FUNCTION "init_square_matrix"("dim_p" INTEGER)
2395 RETURNS INT4[][]
2396 LANGUAGE 'plpgsql' IMMUTABLE AS $$
2397 DECLARE
2398 "i" INTEGER;
2399 "row_text_v" TEXT;
2400 "ary_text_v" TEXT;
2401 BEGIN
2402 IF "dim_p" >= 1 THEN
2403 "row_text_v" := '{NULL';
2404 "i" := "dim_p";
2405 LOOP
2406 "i" := "i" - 1;
2407 EXIT WHEN "i" = 0;
2408 "row_text_v" := "row_text_v" || ',NULL';
2409 END LOOP;
2410 "row_text_v" := "row_text_v" || '}';
2411 "ary_text_v" := '{' || "row_text_v";
2412 "i" := "dim_p";
2413 LOOP
2414 "i" := "i" - 1;
2415 EXIT WHEN "i" = 0;
2416 "ary_text_v" := "ary_text_v" || ',' || "row_text_v";
2417 END LOOP;
2418 "ary_text_v" := "ary_text_v" || '}';
2419 RETURN "ary_text_v"::INT4[][];
2420 ELSE
2421 RAISE EXCEPTION 'Dimension needs to be at least 1.';
2422 END IF;
2423 END;
2424 $$;
2426 COMMENT ON FUNCTION "init_square_matrix"(INTEGER) IS 'Needed for PostgreSQL < 8.4, due to missing "array_fill" function';
2429 CREATE FUNCTION "calculate_ranks"("issue_id_p" "issue"."id"%TYPE)
2430 RETURNS VOID
2431 LANGUAGE 'plpgsql' VOLATILE AS $$
2432 DECLARE
2433 "dimension_v" INTEGER;
2434 "matrix" INT4[][];
2435 "i" INTEGER;
2436 "j" INTEGER;
2437 "k" INTEGER;
2438 "battle_row" "battle"%ROWTYPE;
2439 "rank_ary" INT4[];
2440 "rank_v" INT4;
2441 "done_v" INTEGER;
2442 "winners_ary" INTEGER[];
2443 "initiative_id_v" "initiative"."id"%TYPE;
2444 BEGIN
2445 PERFORM NULL FROM "issue" WHERE "id" = "issue_id_p" FOR UPDATE;
2446 -- Prepare matrix for Schulze-Method:
2447 SELECT count(1) INTO "dimension_v"
2448 FROM "battle_participant" WHERE "issue_id" = "issue_id_p";
2449 IF "dimension_v" = 1 THEN
2450 UPDATE "initiative" SET
2451 "rank" = 1
2452 FROM "battle_participant"
2453 WHERE "initiative"."issue_id" = "issue_id_p"
2454 AND "initiative"."id" = "battle_participant"."initiative_id";
2455 ELSIF "dimension_v" > 1 THEN
2456 "matrix" := "init_square_matrix"("dimension_v"); -- TODO: replace by "array_fill" function (PostgreSQL 8.4)
2457 "i" := 1;
2458 "j" := 2;
2459 -- Fill matrix with data from "battle" view
2460 FOR "battle_row" IN
2461 SELECT * FROM "battle" WHERE "issue_id" = "issue_id_p"
2462 ORDER BY "winning_initiative_id", "losing_initiative_id"
2463 LOOP
2464 "matrix"["i"]["j"] := "battle_row"."count";
2465 IF "j" = "dimension_v" THEN
2466 "i" := "i" + 1;
2467 "j" := 1;
2468 ELSE
2469 "j" := "j" + 1;
2470 IF "j" = "i" THEN
2471 "j" := "j" + 1;
2472 END IF;
2473 END IF;
2474 END LOOP;
2475 IF "i" != "dimension_v" OR "j" != "dimension_v" + 1 THEN
2476 RAISE EXCEPTION 'Wrong battle count (should not happen)';
2477 END IF;
2478 -- Delete losers from matrix:
2479 "i" := 1;
2480 LOOP
2481 "j" := "i" + 1;
2482 LOOP
2483 IF "i" != "j" THEN
2484 IF "matrix"["i"]["j"] < "matrix"["j"]["i"] THEN
2485 "matrix"["i"]["j"] := 0;
2486 ELSIF matrix[j][i] < matrix[i][j] THEN
2487 "matrix"["j"]["i"] := 0;
2488 ELSE
2489 "matrix"["i"]["j"] := 0;
2490 "matrix"["j"]["i"] := 0;
2491 END IF;
2492 END IF;
2493 EXIT WHEN "j" = "dimension_v";
2494 "j" := "j" + 1;
2495 END LOOP;
2496 EXIT WHEN "i" = "dimension_v" - 1;
2497 "i" := "i" + 1;
2498 END LOOP;
2499 -- Find best paths:
2500 "i" := 1;
2501 LOOP
2502 "j" := 1;
2503 LOOP
2504 IF "i" != "j" THEN
2505 "k" := 1;
2506 LOOP
2507 IF "i" != "k" AND "j" != "k" THEN
2508 IF "matrix"["j"]["i"] < "matrix"["i"]["k"] THEN
2509 IF "matrix"["j"]["i"] > "matrix"["j"]["k"] THEN
2510 "matrix"["j"]["k"] := "matrix"["j"]["i"];
2511 END IF;
2512 ELSE
2513 IF "matrix"["i"]["k"] > "matrix"["j"]["k"] THEN
2514 "matrix"["j"]["k"] := "matrix"["i"]["k"];
2515 END IF;
2516 END IF;
2517 END IF;
2518 EXIT WHEN "k" = "dimension_v";
2519 "k" := "k" + 1;
2520 END LOOP;
2521 END IF;
2522 EXIT WHEN "j" = "dimension_v";
2523 "j" := "j" + 1;
2524 END LOOP;
2525 EXIT WHEN "i" = "dimension_v";
2526 "i" := "i" + 1;
2527 END LOOP;
2528 -- Determine order of winners:
2529 "rank_ary" := "init_array"("dimension_v"); -- TODO: replace by "array_fill" function (PostgreSQL 8.4)
2530 "rank_v" := 1;
2531 "done_v" := 0;
2532 LOOP
2533 "winners_ary" := '{}';
2534 "i" := 1;
2535 LOOP
2536 IF "rank_ary"["i"] ISNULL THEN
2537 "j" := 1;
2538 LOOP
2539 IF
2540 "i" != "j" AND
2541 "rank_ary"["j"] ISNULL AND
2542 "matrix"["j"]["i"] > "matrix"["i"]["j"]
2543 THEN
2544 -- someone else is better
2545 EXIT;
2546 END IF;
2547 IF "j" = "dimension_v" THEN
2548 -- noone is better
2549 "winners_ary" := "winners_ary" || "i";
2550 EXIT;
2551 END IF;
2552 "j" := "j" + 1;
2553 END LOOP;
2554 END IF;
2555 EXIT WHEN "i" = "dimension_v";
2556 "i" := "i" + 1;
2557 END LOOP;
2558 "i" := 1;
2559 LOOP
2560 "rank_ary"["winners_ary"["i"]] := "rank_v";
2561 "done_v" := "done_v" + 1;
2562 EXIT WHEN "i" = array_upper("winners_ary", 1);
2563 "i" := "i" + 1;
2564 END LOOP;
2565 EXIT WHEN "done_v" = "dimension_v";
2566 "rank_v" := "rank_v" + 1;
2567 END LOOP;
2568 -- write preliminary ranks:
2569 "i" := 1;
2570 FOR "initiative_id_v" IN
2571 SELECT "initiative"."id"
2572 FROM "initiative" JOIN "battle_participant"
2573 ON "initiative"."id" = "battle_participant"."initiative_id"
2574 WHERE "initiative"."issue_id" = "issue_id_p"
2575 ORDER BY "initiative"."id"
2576 LOOP
2577 UPDATE "initiative" SET "rank" = "rank_ary"["i"]
2578 WHERE "id" = "initiative_id_v";
2579 "i" := "i" + 1;
2580 END LOOP;
2581 IF "i" != "dimension_v" + 1 THEN
2582 RAISE EXCEPTION 'Wrong winner count (should not happen)';
2583 END IF;
2584 -- straighten ranks (start counting with 1, no equal ranks):
2585 "rank_v" := 1;
2586 FOR "initiative_id_v" IN
2587 SELECT "id" FROM "initiative"
2588 WHERE "issue_id" = "issue_id_p" AND "rank" NOTNULL
2589 ORDER BY
2590 "rank",
2591 "vote_ratio"("positive_votes", "negative_votes") DESC,
2592 "id"
2593 LOOP
2594 UPDATE "initiative" SET "rank" = "rank_v"
2595 WHERE "id" = "initiative_id_v";
2596 "rank_v" := "rank_v" + 1;
2597 END LOOP;
2598 END IF;
2599 -- mark issue as finished
2600 UPDATE "issue" SET "ranks_available" = TRUE
2601 WHERE "id" = "issue_id_p";
2602 RETURN;
2603 END;
2604 $$;
2606 COMMENT ON FUNCTION "calculate_ranks"
2607 ( "issue"."id"%TYPE )
2608 IS 'Determine ranking (Votes have to be counted first)';
2612 -----------------------------
2613 -- Automatic state changes --
2614 -----------------------------
2617 CREATE FUNCTION "check_issue"
2618 ( "issue_id_p" "issue"."id"%TYPE )
2619 RETURNS VOID
2620 LANGUAGE 'plpgsql' VOLATILE AS $$
2621 DECLARE
2622 "issue_row" "issue"%ROWTYPE;
2623 "policy_row" "policy"%ROWTYPE;
2624 "voting_requested_v" BOOLEAN;
2625 BEGIN
2626 PERFORM "global_lock"();
2627 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
2628 IF "issue_row"."closed" ISNULL THEN
2629 SELECT * INTO "policy_row" FROM "policy"
2630 WHERE "id" = "issue_row"."policy_id";
2631 IF "issue_row"."fully_frozen" ISNULL THEN
2632 PERFORM "create_snapshot"("issue_id_p");
2633 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
2634 END IF;
2635 IF "issue_row"."accepted" ISNULL THEN
2636 IF EXISTS (
2637 SELECT NULL FROM "initiative"
2638 WHERE "issue_id" = "issue_id_p"
2639 AND "supporter_count" > 0
2640 AND "supporter_count" * "policy_row"."issue_quorum_den"
2641 >= "issue_row"."population" * "policy_row"."issue_quorum_num"
2642 ) THEN
2643 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
2644 "issue_row"."accepted" = now(); -- NOTE: "issue_row" used later
2645 UPDATE "issue" SET "accepted" = "issue_row"."accepted"
2646 WHERE "id" = "issue_row"."id";
2647 ELSIF
2648 now() >= "issue_row"."created" + "policy_row"."admission_time"
2649 THEN
2650 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
2651 UPDATE "issue" SET "closed" = now()
2652 WHERE "id" = "issue_row"."id";
2653 END IF;
2654 END IF;
2655 IF
2656 "issue_row"."accepted" NOTNULL AND
2657 "issue_row"."half_frozen" ISNULL
2658 THEN
2659 SELECT
2660 CASE
2661 WHEN "vote_now" * 2 > "issue_row"."population" THEN
2662 TRUE
2663 WHEN "vote_later" * 2 > "issue_row"."population" THEN
2664 FALSE
2665 ELSE NULL
2666 END
2667 INTO "voting_requested_v"
2668 FROM "issue" WHERE "id" = "issue_id_p";
2669 IF
2670 "voting_requested_v" OR (
2671 "voting_requested_v" ISNULL AND
2672 now() >= "issue_row"."accepted" + "policy_row"."discussion_time"
2674 THEN
2675 "issue_row"."half_frozen" = now(); -- NOTE: "issue_row" used later
2676 UPDATE "issue" SET "half_frozen" = "issue_row"."half_frozen"
2677 WHERE "id" = "issue_row"."id";
2678 END IF;
2679 END IF;
2680 IF
2681 "issue_row"."half_frozen" NOTNULL AND
2682 "issue_row"."fully_frozen" ISNULL AND
2683 now() >= "issue_row"."half_frozen" + "policy_row"."verification_time"
2684 THEN
2685 PERFORM "freeze_after_snapshot"("issue_id_p");
2686 -- "issue" might change, thus "issue_row" has to be updated below
2687 END IF;
2688 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
2689 IF
2690 "issue_row"."closed" ISNULL AND
2691 "issue_row"."fully_frozen" NOTNULL AND
2692 now() >= "issue_row"."fully_frozen" + "policy_row"."voting_time"
2693 THEN
2694 PERFORM "close_voting"("issue_id_p");
2695 END IF;
2696 END IF;
2697 RETURN;
2698 END;
2699 $$;
2701 COMMENT ON FUNCTION "check_issue"
2702 ( "issue"."id"%TYPE )
2703 IS 'Precalculate supporter counts etc. for a given issue, and check, if status change is required; At end of voting the ranking is not calculated by this function, but must be calculated in a seperate transaction using the "calculate_ranks" function.';
2706 CREATE FUNCTION "check_everything"()
2707 RETURNS VOID
2708 LANGUAGE 'plpgsql' VOLATILE AS $$
2709 DECLARE
2710 "issue_id_v" "issue"."id"%TYPE;
2711 BEGIN
2712 DELETE FROM "expired_session";
2713 PERFORM "calculate_member_counts"();
2714 FOR "issue_id_v" IN SELECT "id" FROM "open_issue" LOOP
2715 PERFORM "check_issue"("issue_id_v");
2716 END LOOP;
2717 FOR "issue_id_v" IN SELECT "id" FROM "issue_with_ranks_missing" LOOP
2718 PERFORM "calculate_ranks"("issue_id_v");
2719 END LOOP;
2720 RETURN;
2721 END;
2722 $$;
2724 COMMENT ON FUNCTION "check_everything"() IS 'Perform "check_issue" for every open issue, and if possible, automatically calculate ranks. Use this function only for development and debugging purposes, as long transactions with exclusive locking may result.';
2728 ------------------------------
2729 -- Deletion of private data --
2730 ------------------------------
2733 CREATE FUNCTION "delete_private_data"()
2734 RETURNS VOID
2735 LANGUAGE 'plpgsql' VOLATILE AS $$
2736 DECLARE
2737 "issue_id_v" "issue"."id"%TYPE;
2738 BEGIN
2739 UPDATE "member" SET
2740 "login" = 'login' || "id"::text,
2741 "password" = NULL,
2742 "notify_email" = NULL,
2743 "notify_email_confirmed" = NULL;
2744 DELETE FROM "session";
2745 DELETE FROM "invite_code";
2746 DELETE FROM "contact" WHERE NOT "public";
2747 DELETE FROM "direct_voter" USING "issue"
2748 WHERE "direct_voter"."issue_id" = "issue"."id"
2749 AND "issue"."closed" ISNULL;
2750 RETURN;
2751 END;
2752 $$;
2754 COMMENT ON FUNCTION "delete_private_data"() IS 'DO NOT USE on productive database, but only on a copy! This function deletes all data which should not be publicly available, and can be used to create a database dump for publication.';
2758 COMMIT;

Impressum / About Us