liquid_feedback_core
view core.sql @ 182:4c03ef70dd92
Removed trigger "set_activated_timestamp"; Updated update script
author | jbe |
---|---|
date | Fri Jul 29 15:23:42 2011 +0200 (2011-07-29) |
parents | e3b0ea7ab2ad |
children | ed2f94a397cd |
line source
2 -- Execute the following command manually for PostgreSQL prior version 9.0:
3 -- CREATE LANGUAGE plpgsql;
5 -- NOTE: In PostgreSQL every UNIQUE constraint implies creation of an index
7 BEGIN;
9 CREATE VIEW "liquid_feedback_version" AS
10 SELECT * FROM (VALUES ('1.5.0_devel', 1, 5, -1))
11 AS "subquery"("string", "major", "minor", "revision");
15 ----------------------
16 -- Full text search --
17 ----------------------
20 CREATE FUNCTION "text_search_query"("query_text_p" TEXT)
21 RETURNS TSQUERY
22 LANGUAGE 'plpgsql' IMMUTABLE AS $$
23 BEGIN
24 RETURN plainto_tsquery('pg_catalog.simple', "query_text_p");
25 END;
26 $$;
28 COMMENT ON FUNCTION "text_search_query"(TEXT) IS 'Usage: WHERE "text_search_data" @@ "text_search_query"(''<user query>'')';
31 CREATE FUNCTION "highlight"
32 ( "body_p" TEXT,
33 "query_text_p" TEXT )
34 RETURNS TEXT
35 LANGUAGE 'plpgsql' IMMUTABLE AS $$
36 BEGIN
37 RETURN ts_headline(
38 'pg_catalog.simple',
39 replace(replace("body_p", e'\\', e'\\\\'), '*', e'\\*'),
40 "text_search_query"("query_text_p"),
41 'StartSel=* StopSel=* HighlightAll=TRUE' );
42 END;
43 $$;
45 COMMENT ON FUNCTION "highlight"
46 ( "body_p" TEXT,
47 "query_text_p" TEXT )
48 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.';
52 -------------------------
53 -- Tables and indicies --
54 -------------------------
57 CREATE TABLE "system_setting" (
58 "member_ttl" INTERVAL );
59 CREATE UNIQUE INDEX "system_setting_singleton_idx" ON "system_setting" ((1));
61 COMMENT ON TABLE "system_setting" IS 'This table contains only one row with different settings in each column.';
62 COMMENT ON INDEX "system_setting_singleton_idx" IS 'This index ensures that "system_setting" only contains one row maximum.';
64 COMMENT ON COLUMN "system_setting"."member_ttl" IS 'Time after members get their "active" flag set to FALSE, if they do not login anymore.';
67 CREATE TABLE "contingent" (
68 "time_frame" INTERVAL PRIMARY KEY,
69 "text_entry_limit" INT4,
70 "initiative_limit" INT4 );
72 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.';
74 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';
75 COMMENT ON COLUMN "contingent"."initiative_limit" IS 'Number of new initiatives to be opened by each member within a given time frame';
78 CREATE TYPE "notify_level" AS ENUM
79 ('none', 'voting', 'verification', 'discussion', 'all');
81 COMMENT ON TYPE "notify_level" IS 'Level of notification: ''none'' = no notifications, ''voting'' = notifications about finished issues and issues in voting, ''verification'' = notifications about finished issues, issues in voting and verification phase, ''discussion'' = notifications about everything except issues in admission phase, ''all'' = notifications about everything';
84 CREATE TABLE "member" (
85 "id" SERIAL4 PRIMARY KEY,
86 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
87 "invite_code" TEXT UNIQUE,
88 "admin_comment" TEXT,
89 "activated" TIMESTAMPTZ,
90 "last_login" TIMESTAMPTZ,
91 "last_login_public" DATE,
92 "login" TEXT UNIQUE,
93 "password" TEXT,
94 "locked" BOOLEAN NOT NULL DEFAULT FALSE,
95 "active" BOOLEAN NOT NULL DEFAULT FALSE,
96 "admin" BOOLEAN NOT NULL DEFAULT FALSE,
97 "notify_email" TEXT,
98 "notify_email_unconfirmed" TEXT,
99 "notify_email_secret" TEXT UNIQUE,
100 "notify_email_secret_expiry" TIMESTAMPTZ,
101 "notify_email_lock_expiry" TIMESTAMPTZ,
102 "notify_level" "notify_level" NOT NULL DEFAULT 'none',
103 "notify_event_id" INT8,
104 "password_reset_secret" TEXT UNIQUE,
105 "password_reset_secret_expiry" TIMESTAMPTZ,
106 "name" TEXT NOT NULL UNIQUE,
107 "identification" TEXT UNIQUE,
108 "organizational_unit" TEXT,
109 "internal_posts" TEXT,
110 "realname" TEXT,
111 "birthday" DATE,
112 "address" TEXT,
113 "email" TEXT,
114 "xmpp_address" TEXT,
115 "website" TEXT,
116 "phone" TEXT,
117 "mobile_phone" TEXT,
118 "profession" TEXT,
119 "external_memberships" TEXT,
120 "external_posts" TEXT,
121 "formatting_engine" TEXT,
122 "statement" TEXT,
123 "text_search_data" TSVECTOR,
124 CONSTRAINT "not_active_without_activated"
125 CHECK ("activated" NOTNULL OR "active" = FALSE) );
126 CREATE INDEX "member_active_idx" ON "member" ("active");
127 CREATE INDEX "member_text_search_data_idx" ON "member" USING gin ("text_search_data");
128 CREATE TRIGGER "update_text_search_data"
129 BEFORE INSERT OR UPDATE ON "member"
130 FOR EACH ROW EXECUTE PROCEDURE
131 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
132 "name", "identification", "organizational_unit", "internal_posts",
133 "realname", "external_memberships", "external_posts", "statement" );
135 COMMENT ON TABLE "member" IS 'Users of the system, e.g. members of an organization';
137 COMMENT ON COLUMN "member"."created" IS 'Creation of member record and/or invite code';
138 COMMENT ON COLUMN "member"."invite_code" IS 'Optional invite code, to allow a member to initialize his/her account the first time';
139 COMMENT ON COLUMN "member"."admin_comment" IS 'Hidden comment for administrative purposes';
140 COMMENT ON COLUMN "member"."activated" IS 'Timestamp of activation of account (i.e. usage of "invite_code"); needs to be set for "active" members';
141 COMMENT ON COLUMN "member"."last_login" IS 'Timestamp of last login';
142 COMMENT ON COLUMN "member"."last_login_public" IS 'Date of last login (time stripped for privacy reasons, updated only after day change)';
143 COMMENT ON COLUMN "member"."login" IS 'Login name';
144 COMMENT ON COLUMN "member"."password" IS 'Password (preferably as crypto-hash, depending on the frontend or access layer)';
145 COMMENT ON COLUMN "member"."locked" IS 'Locked members can not log in.';
146 COMMENT ON COLUMN "member"."active" IS 'Memberships, support and votes are taken into account when corresponding members are marked as active. When the user does not log in for an extended period of time, this flag may be set to FALSE. If the user is not locked, he/she may reset the active flag by logging in (has to be set to TRUE by frontend on every login).';
147 COMMENT ON COLUMN "member"."admin" IS 'TRUE for admins, which can administrate other users and setup policies and areas';
148 COMMENT ON COLUMN "member"."notify_email" IS 'Email address where notifications of the system are sent to';
149 COMMENT ON COLUMN "member"."notify_email_unconfirmed" IS 'Unconfirmed email address provided by the member to be copied into "notify_email" field after verification';
150 COMMENT ON COLUMN "member"."notify_email_secret" IS 'Secret sent to the address in "notify_email_unconformed"';
151 COMMENT ON COLUMN "member"."notify_email_secret_expiry" IS 'Expiry date/time for "notify_email_secret"';
152 COMMENT ON COLUMN "member"."notify_email_lock_expiry" IS 'Date/time until no further email confirmation mails may be sent (abuse protection)';
153 COMMENT ON COLUMN "member"."notify_level" IS 'Selects which event notifications are to be sent to the "notify_email" mail address';
154 COMMENT ON COLUMN "member"."notify_event_id" IS 'Latest "id" of an "event" the member was notified about';
155 COMMENT ON COLUMN "member"."name" IS 'Distinct name of the member';
156 COMMENT ON COLUMN "member"."identification" IS 'Optional identification number or code of the member';
157 COMMENT ON COLUMN "member"."organizational_unit" IS 'Branch or division of the organization the member belongs to';
158 COMMENT ON COLUMN "member"."internal_posts" IS 'Posts (offices) of the member inside the organization';
159 COMMENT ON COLUMN "member"."realname" IS 'Real name of the member, may be identical with "name"';
160 COMMENT ON COLUMN "member"."email" IS 'Published email address of the member; not used for system notifications';
161 COMMENT ON COLUMN "member"."external_memberships" IS 'Other organizations the member is involved in';
162 COMMENT ON COLUMN "member"."external_posts" IS 'Posts (offices) outside the organization';
163 COMMENT ON COLUMN "member"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used for "member"."statement"';
164 COMMENT ON COLUMN "member"."statement" IS 'Freely chosen text of the member for his homepage within the system';
167 CREATE TABLE "member_history" (
168 "id" SERIAL8 PRIMARY KEY,
169 "member_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
170 "until" TIMESTAMPTZ NOT NULL DEFAULT now(),
171 "active" BOOLEAN NOT NULL,
172 "name" TEXT NOT NULL );
173 CREATE INDEX "member_history_member_id_idx" ON "member_history" ("member_id");
175 COMMENT ON TABLE "member_history" IS 'Filled by trigger; keeps information about old names and active flag of members';
177 COMMENT ON COLUMN "member_history"."id" IS 'Primary key, which can be used to sort entries correctly (and time warp resistant)';
178 COMMENT ON COLUMN "member_history"."until" IS 'Timestamp until the data was valid';
181 CREATE TABLE "rendered_member_statement" (
182 PRIMARY KEY ("member_id", "format"),
183 "member_id" INT8 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
184 "format" TEXT,
185 "content" TEXT NOT NULL );
187 COMMENT ON TABLE "rendered_member_statement" IS 'This table may be used by frontends to cache "rendered" member statements (e.g. HTML output generated from wiki text)';
190 CREATE TABLE "setting" (
191 PRIMARY KEY ("member_id", "key"),
192 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
193 "key" TEXT NOT NULL,
194 "value" TEXT NOT NULL );
195 CREATE INDEX "setting_key_idx" ON "setting" ("key");
197 COMMENT ON TABLE "setting" IS 'Place to store a frontend specific setting for members as a string';
199 COMMENT ON COLUMN "setting"."key" IS 'Name of the setting, preceded by a frontend specific prefix';
202 CREATE TABLE "setting_map" (
203 PRIMARY KEY ("member_id", "key", "subkey"),
204 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
205 "key" TEXT NOT NULL,
206 "subkey" TEXT NOT NULL,
207 "value" TEXT NOT NULL );
208 CREATE INDEX "setting_map_key_idx" ON "setting_map" ("key");
210 COMMENT ON TABLE "setting_map" IS 'Place to store a frontend specific setting for members as a map of key value pairs';
212 COMMENT ON COLUMN "setting_map"."key" IS 'Name of the setting, preceded by a frontend specific prefix';
213 COMMENT ON COLUMN "setting_map"."subkey" IS 'Key of a map entry';
214 COMMENT ON COLUMN "setting_map"."value" IS 'Value of a map entry';
217 CREATE TABLE "member_relation_setting" (
218 PRIMARY KEY ("member_id", "key", "other_member_id"),
219 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
220 "key" TEXT NOT NULL,
221 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
222 "value" TEXT NOT NULL );
224 COMMENT ON TABLE "member_relation_setting" IS 'Place to store a frontend specific setting related to relations between members as a string';
227 CREATE TYPE "member_image_type" AS ENUM ('photo', 'avatar');
229 COMMENT ON TYPE "member_image_type" IS 'Types of images for a member';
232 CREATE TABLE "member_image" (
233 PRIMARY KEY ("member_id", "image_type", "scaled"),
234 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
235 "image_type" "member_image_type",
236 "scaled" BOOLEAN,
237 "content_type" TEXT,
238 "data" BYTEA NOT NULL );
240 COMMENT ON TABLE "member_image" IS 'Images of members';
242 COMMENT ON COLUMN "member_image"."scaled" IS 'FALSE for original image, TRUE for scaled version of the image';
245 CREATE TABLE "member_count" (
246 "calculated" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
247 "total_count" INT4 NOT NULL );
249 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';
251 COMMENT ON COLUMN "member_count"."calculated" IS 'timestamp indicating when the total member count and area member counts were calculated';
252 COMMENT ON COLUMN "member_count"."total_count" IS 'Total count of active(!) members';
255 CREATE TABLE "contact" (
256 PRIMARY KEY ("member_id", "other_member_id"),
257 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
258 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
259 "public" BOOLEAN NOT NULL DEFAULT FALSE,
260 CONSTRAINT "cant_save_yourself_as_contact"
261 CHECK ("member_id" != "other_member_id") );
262 CREATE INDEX "contact_other_member_id_idx" ON "contact" ("other_member_id");
264 COMMENT ON TABLE "contact" IS 'Contact lists';
266 COMMENT ON COLUMN "contact"."member_id" IS 'Member having the contact list';
267 COMMENT ON COLUMN "contact"."other_member_id" IS 'Member referenced in the contact list';
268 COMMENT ON COLUMN "contact"."public" IS 'TRUE = display contact publically';
271 CREATE TABLE "ignored_member" (
272 PRIMARY KEY ("member_id", "other_member_id"),
273 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
274 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
275 CREATE INDEX "ignored_member_other_member_id_idx" ON "ignored_member" ("other_member_id");
277 COMMENT ON TABLE "ignored_member" IS 'Possibility to filter other members';
279 COMMENT ON COLUMN "ignored_member"."member_id" IS 'Member ignoring someone';
280 COMMENT ON COLUMN "ignored_member"."other_member_id" IS 'Member being ignored';
283 CREATE TABLE "session" (
284 "ident" TEXT PRIMARY KEY,
285 "additional_secret" TEXT,
286 "expiry" TIMESTAMPTZ NOT NULL DEFAULT now() + '24 hours',
287 "member_id" INT8 REFERENCES "member" ("id") ON DELETE SET NULL,
288 "lang" TEXT );
289 CREATE INDEX "session_expiry_idx" ON "session" ("expiry");
291 COMMENT ON TABLE "session" IS 'Sessions, i.e. for a web-frontend';
293 COMMENT ON COLUMN "session"."ident" IS 'Secret session identifier (i.e. random string)';
294 COMMENT ON COLUMN "session"."additional_secret" IS 'Additional field to store a secret, which can be used against CSRF attacks';
295 COMMENT ON COLUMN "session"."member_id" IS 'Reference to member, who is logged in';
296 COMMENT ON COLUMN "session"."lang" IS 'Language code of the selected language';
299 CREATE TABLE "policy" (
300 "id" SERIAL4 PRIMARY KEY,
301 "index" INT4 NOT NULL,
302 "active" BOOLEAN NOT NULL DEFAULT TRUE,
303 "name" TEXT NOT NULL UNIQUE,
304 "description" TEXT NOT NULL DEFAULT '',
305 "admission_time" INTERVAL NOT NULL,
306 "discussion_time" INTERVAL NOT NULL,
307 "verification_time" INTERVAL NOT NULL,
308 "voting_time" INTERVAL NOT NULL,
309 "issue_quorum_num" INT4 NOT NULL,
310 "issue_quorum_den" INT4 NOT NULL,
311 "initiative_quorum_num" INT4 NOT NULL,
312 "initiative_quorum_den" INT4 NOT NULL,
313 "direct_majority_num" INT4 NOT NULL DEFAULT 1,
314 "direct_majority_den" INT4 NOT NULL DEFAULT 2,
315 "direct_majority_strict" BOOLEAN NOT NULL DEFAULT TRUE,
316 "direct_majority_positive" INT4 NOT NULL DEFAULT 0,
317 "direct_majority_non_negative" INT4 NOT NULL DEFAULT 0,
318 "indirect_majority_num" INT4 NOT NULL DEFAULT 1,
319 "indirect_majority_den" INT4 NOT NULL DEFAULT 2,
320 "indirect_majority_strict" BOOLEAN NOT NULL DEFAULT TRUE,
321 "indirect_majority_positive" INT4 NOT NULL DEFAULT 0,
322 "indirect_majority_non_negative" INT4 NOT NULL DEFAULT 0,
323 "no_reverse_beat_path" BOOLEAN NOT NULL DEFAULT TRUE,
324 "no_multistage_majority" BOOLEAN NOT NULL DEFAULT FALSE );
325 CREATE INDEX "policy_active_idx" ON "policy" ("active");
327 COMMENT ON TABLE "policy" IS 'Policies for a particular proceeding type (timelimits, quorum)';
329 COMMENT ON COLUMN "policy"."index" IS 'Determines the order in listings';
330 COMMENT ON COLUMN "policy"."active" IS 'TRUE = policy can be used for new issues';
331 COMMENT ON COLUMN "policy"."admission_time" IS 'Maximum time an issue stays open without being "accepted"';
332 COMMENT ON COLUMN "policy"."discussion_time" IS 'Regular time until an issue is "half_frozen" after being "accepted"';
333 COMMENT ON COLUMN "policy"."verification_time" IS 'Regular time until an issue is "fully_frozen" after being "half_frozen"';
334 COMMENT ON COLUMN "policy"."voting_time" IS 'Time after an issue is "fully_frozen" but not "closed"';
335 COMMENT ON COLUMN "policy"."issue_quorum_num" IS 'Numerator of potential supporter quorum to be reached by one initiative of an issue to be "accepted"';
336 COMMENT ON COLUMN "policy"."issue_quorum_den" IS 'Denominator of potential supporter quorum to be reached by one initiative of an issue to be "accepted"';
337 COMMENT ON COLUMN "policy"."initiative_quorum_num" IS 'Numerator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
338 COMMENT ON COLUMN "policy"."initiative_quorum_den" IS 'Denominator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
339 COMMENT ON COLUMN "policy"."direct_majority_num" IS 'Numerator of fraction of neccessary direct majority for initiatives to be attainable as winner';
340 COMMENT ON COLUMN "policy"."direct_majority_den" IS 'Denominator of fraction of neccessary direct majority for initaitives to be attainable as winner';
341 COMMENT ON COLUMN "policy"."direct_majority_strict" IS 'If TRUE, then the direct majority must be strictly greater than "direct_majority_num"/"direct_majority_den", otherwise it may also be equal.';
342 COMMENT ON COLUMN "policy"."direct_majority_positive" IS 'Absolute number of "positive_votes" neccessary for an initiative to be attainable as winner';
343 COMMENT ON COLUMN "policy"."direct_majority_non_negative" IS 'Absolute number of sum of "positive_votes" and abstentions neccessary for an initiative to be attainable as winner';
344 COMMENT ON COLUMN "policy"."indirect_majority_num" IS 'Numerator of fraction of neccessary indirect majority (through beat path) for initiatives to be attainable as winner';
345 COMMENT ON COLUMN "policy"."indirect_majority_den" IS 'Denominator of fraction of neccessary indirect majority (through beat path) for initiatives to be attainable as winner';
346 COMMENT ON COLUMN "policy"."indirect_majority_strict" IS 'If TRUE, then the indirect majority must be strictly greater than "indirect_majority_num"/"indirect_majority_den", otherwise it may also be equal.';
347 COMMENT ON COLUMN "policy"."indirect_majority_positive" IS 'Absolute number of votes in favor of the winner neccessary in a beat path to the status quo for an initaitive to be attainable as winner';
348 COMMENT ON COLUMN "policy"."indirect_majority_non_negative" IS 'Absolute number of sum of votes in favor and abstentions in a beat path to the status quo for an initiative to be attainable as winner';
349 COMMENT ON COLUMN "policy"."no_reverse_beat_path" IS 'Causes initiatives with "reverse_beat_path" flag to not be "eligible", thus disallowing them to be winner. See comment on column "initiative"."reverse_beat_path". This option ensures both that a winning initiative is never tied in a (weak) condorcet paradox with the status quo and a winning initiative always beats the status quo directly with a simple majority.';
350 COMMENT ON COLUMN "policy"."no_multistage_majority" IS 'Causes initiatives with "multistage_majority" flag to not be "eligible", thus disallowing them to be winner. See comment on column "initiative"."multistage_majority". This disqualifies initiatives which could cause an instable result. An instable result in this meaning is a result such that repeating the ballot with same preferences but with the winner of the first ballot as status quo would lead to a different winner in the second ballot. If there are no direct majorities required for the winner, or if in direct comparison only simple majorities are required and "no_reverse_beat_path" is true, then results are always stable and this flag does not have any effect on the winner (but still affects the "eligible" flag of an "initiative").';
353 CREATE TABLE "unit" (
354 "id" SERIAL4 PRIMARY KEY,
355 "parent_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
356 "active" BOOLEAN NOT NULL DEFAULT TRUE,
357 "name" TEXT NOT NULL,
358 "description" TEXT NOT NULL DEFAULT '',
359 "member_count" INT4,
360 "text_search_data" TSVECTOR );
361 CREATE INDEX "unit_root_idx" ON "unit" ("id") WHERE "parent_id" ISNULL;
362 CREATE INDEX "unit_parent_id_idx" ON "unit" ("parent_id");
363 CREATE INDEX "unit_active_idx" ON "unit" ("active");
364 CREATE INDEX "unit_text_search_data_idx" ON "unit" USING gin ("text_search_data");
365 CREATE TRIGGER "update_text_search_data"
366 BEFORE INSERT OR UPDATE ON "unit"
367 FOR EACH ROW EXECUTE PROCEDURE
368 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
369 "name", "description" );
371 COMMENT ON TABLE "unit" IS 'Organizational units organized as trees; Delegations are not inherited through these trees.';
373 COMMENT ON COLUMN "unit"."parent_id" IS 'Parent id of tree node; Multiple roots allowed';
374 COMMENT ON COLUMN "unit"."active" IS 'TRUE means new issues can be created in units of this area';
375 COMMENT ON COLUMN "unit"."member_count" IS 'Count of members as determined by column "voting_right" in table "privilege"';
378 CREATE TABLE "area" (
379 "id" SERIAL4 PRIMARY KEY,
380 "unit_id" INT4 NOT NULL REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
381 "active" BOOLEAN NOT NULL DEFAULT TRUE,
382 "name" TEXT NOT NULL,
383 "description" TEXT NOT NULL DEFAULT '',
384 "direct_member_count" INT4,
385 "member_weight" INT4,
386 "text_search_data" TSVECTOR );
387 CREATE INDEX "area_unit_id_idx" ON "area" ("unit_id");
388 CREATE INDEX "area_active_idx" ON "area" ("active");
389 CREATE INDEX "area_text_search_data_idx" ON "area" USING gin ("text_search_data");
390 CREATE TRIGGER "update_text_search_data"
391 BEFORE INSERT OR UPDATE ON "area"
392 FOR EACH ROW EXECUTE PROCEDURE
393 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
394 "name", "description" );
396 COMMENT ON TABLE "area" IS 'Subject areas';
398 COMMENT ON COLUMN "area"."active" IS 'TRUE means new issues can be created in this area';
399 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"';
400 COMMENT ON COLUMN "area"."member_weight" IS 'Same as "direct_member_count" but respecting delegations';
403 CREATE TABLE "area_setting" (
404 PRIMARY KEY ("member_id", "key", "area_id"),
405 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
406 "key" TEXT NOT NULL,
407 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
408 "value" TEXT NOT NULL );
410 COMMENT ON TABLE "area_setting" IS 'Place for frontend to store area specific settings of members as strings';
413 CREATE TABLE "allowed_policy" (
414 PRIMARY KEY ("area_id", "policy_id"),
415 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
416 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
417 "default_policy" BOOLEAN NOT NULL DEFAULT FALSE );
418 CREATE UNIQUE INDEX "allowed_policy_one_default_per_area_idx" ON "allowed_policy" ("area_id") WHERE "default_policy";
420 COMMENT ON TABLE "allowed_policy" IS 'Selects which policies can be used in each area';
422 COMMENT ON COLUMN "allowed_policy"."default_policy" IS 'One policy per area can be set as default.';
425 CREATE TYPE "snapshot_event" AS ENUM ('periodic', 'end_of_admission', 'half_freeze', 'full_freeze');
427 COMMENT ON TYPE "snapshot_event" IS 'Reason for snapshots: ''periodic'' = due to periodic recalculation, ''end_of_admission'' = saved state at end of admission period, ''half_freeze'' = saved state at end of discussion period, ''full_freeze'' = saved state at end of verification period';
430 CREATE TYPE "issue_state" AS ENUM (
431 'admission', 'discussion', 'verification', 'voting',
432 'canceled_revoked_before_accepted',
433 'canceled_issue_not_accepted',
434 'canceled_after_revocation_during_discussion',
435 'canceled_after_revocation_during_verification',
436 'calculation',
437 'canceled_no_initiative_admitted',
438 'finished_without_winner', 'finished_with_winner');
440 COMMENT ON TYPE "issue_state" IS 'State of issues';
443 CREATE TABLE "issue" (
444 "id" SERIAL4 PRIMARY KEY,
445 "area_id" INT4 NOT NULL REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
446 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
447 "state" "issue_state" NOT NULL DEFAULT 'admission',
448 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
449 "accepted" TIMESTAMPTZ,
450 "half_frozen" TIMESTAMPTZ,
451 "fully_frozen" TIMESTAMPTZ,
452 "closed" TIMESTAMPTZ,
453 "ranks_available" BOOLEAN NOT NULL DEFAULT FALSE,
454 "cleaned" TIMESTAMPTZ,
455 "admission_time" INTERVAL NOT NULL,
456 "discussion_time" INTERVAL NOT NULL,
457 "verification_time" INTERVAL NOT NULL,
458 "voting_time" INTERVAL NOT NULL,
459 "snapshot" TIMESTAMPTZ,
460 "latest_snapshot_event" "snapshot_event",
461 "population" INT4,
462 "voter_count" INT4,
463 "status_quo_schulze_rank" INT4,
464 CONSTRAINT "valid_state" CHECK ((
465 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
466 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
467 ("accepted" NOTNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
468 ("accepted" NOTNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
469 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
470 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
471 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
472 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
473 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = TRUE)) AND (
474 ("state" = 'admission' AND "closed" ISNULL AND "accepted" ISNULL) OR
475 ("state" = 'discussion' AND "closed" ISNULL AND "accepted" NOTNULL AND "half_frozen" ISNULL) OR
476 ("state" = 'verification' AND "closed" ISNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL) OR
477 ("state" = 'voting' AND "closed" ISNULL AND "fully_frozen" NOTNULL) OR
478 ("state" = 'canceled_revoked_before_accepted' AND "closed" NOTNULL AND "accepted" ISNULL) OR
479 ("state" = 'canceled_issue_not_accepted' AND "closed" NOTNULL AND "accepted" ISNULL) OR
480 ("state" = 'canceled_after_revocation_during_discussion' AND "closed" NOTNULL AND "half_frozen" ISNULL) OR
481 ("state" = 'canceled_after_revocation_during_verification' AND "closed" NOTNULL AND "fully_frozen" ISNULL) OR
482 ("state" = 'calculation' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = FALSE) OR
483 ("state" = 'canceled_no_initiative_admitted' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = TRUE) OR
484 ("state" = 'finished_without_winner' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = TRUE) OR
485 ("state" = 'finished_with_winner' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = TRUE)
486 )),
487 CONSTRAINT "state_change_order" CHECK (
488 "created" <= "accepted" AND
489 "accepted" <= "half_frozen" AND
490 "half_frozen" <= "fully_frozen" AND
491 "fully_frozen" <= "closed" ),
492 CONSTRAINT "only_closed_issues_may_be_cleaned" CHECK (
493 "cleaned" ISNULL OR "closed" NOTNULL ),
494 CONSTRAINT "last_snapshot_on_full_freeze"
495 CHECK ("snapshot" = "fully_frozen"), -- NOTE: snapshot can be set, while frozen is NULL yet
496 CONSTRAINT "freeze_requires_snapshot"
497 CHECK ("fully_frozen" ISNULL OR "snapshot" NOTNULL),
498 CONSTRAINT "set_both_or_none_of_snapshot_and_latest_snapshot_event"
499 CHECK ("snapshot" NOTNULL = "latest_snapshot_event" NOTNULL) );
500 CREATE INDEX "issue_area_id_idx" ON "issue" ("area_id");
501 CREATE INDEX "issue_policy_id_idx" ON "issue" ("policy_id");
502 CREATE INDEX "issue_created_idx" ON "issue" ("created");
503 CREATE INDEX "issue_accepted_idx" ON "issue" ("accepted");
504 CREATE INDEX "issue_half_frozen_idx" ON "issue" ("half_frozen");
505 CREATE INDEX "issue_fully_frozen_idx" ON "issue" ("fully_frozen");
506 CREATE INDEX "issue_closed_idx" ON "issue" ("closed");
507 CREATE INDEX "issue_created_idx_open" ON "issue" ("created") WHERE "closed" ISNULL;
508 CREATE INDEX "issue_closed_idx_canceled" ON "issue" ("closed") WHERE "fully_frozen" ISNULL;
510 COMMENT ON TABLE "issue" IS 'Groups of initiatives';
512 COMMENT ON COLUMN "issue"."accepted" IS 'Point in time, when one initiative of issue reached the "issue_quorum"';
513 COMMENT ON COLUMN "issue"."half_frozen" IS 'Point in time, when "discussion_time" has elapsed; Frontends must ensure that for half_frozen issues a) initiatives are not revoked, b) no new drafts are created, c) no initiators are added or removed.';
514 COMMENT ON COLUMN "issue"."fully_frozen" IS 'Point in time, when "verification_time" has elapsed and voting has started; Frontends must ensure that for fully_frozen issues additionally to the restrictions for half_frozen issues a) initiatives are not created, b) no interest is created or removed, c) no supporters are added or removed, d) no opinions are created, changed or deleted.';
515 COMMENT ON COLUMN "issue"."closed" IS 'Point in time, when "admission_time" or "voting_time" have elapsed, and issue is no longer active; Frontends must ensure that for closed issues additionally to the restrictions for half_frozen and fully_frozen issues a) no voter is added or removed to/from the direct_voter table, b) no votes are added, modified or removed.';
516 COMMENT ON COLUMN "issue"."ranks_available" IS 'TRUE = ranks have been calculated';
517 COMMENT ON COLUMN "issue"."cleaned" IS 'Point in time, when discussion data and votes had been deleted';
518 COMMENT ON COLUMN "issue"."admission_time" IS 'Copied from "policy" table at creation of issue';
519 COMMENT ON COLUMN "issue"."discussion_time" IS 'Copied from "policy" table at creation of issue';
520 COMMENT ON COLUMN "issue"."verification_time" IS 'Copied from "policy" table at creation of issue';
521 COMMENT ON COLUMN "issue"."voting_time" IS 'Copied from "policy" table at creation of issue';
522 COMMENT ON COLUMN "issue"."snapshot" IS 'Point in time, when snapshot tables have been updated and "population" and *_count values were precalculated';
523 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';
524 COMMENT ON COLUMN "issue"."population" IS 'Sum of "weight" column in table "direct_population_snapshot"';
525 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';
526 COMMENT ON COLUMN "issue"."status_quo_schulze_rank" IS 'Schulze rank of status quo, as calculated by "calculate_ranks" function';
529 CREATE TABLE "issue_setting" (
530 PRIMARY KEY ("member_id", "key", "issue_id"),
531 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
532 "key" TEXT NOT NULL,
533 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
534 "value" TEXT NOT NULL );
536 COMMENT ON TABLE "issue_setting" IS 'Place for frontend to store issue specific settings of members as strings';
539 CREATE TABLE "initiative" (
540 UNIQUE ("issue_id", "id"), -- index needed for foreign-key on table "vote"
541 "issue_id" INT4 NOT NULL REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
542 "id" SERIAL4 PRIMARY KEY,
543 "name" TEXT NOT NULL,
544 "discussion_url" TEXT,
545 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
546 "revoked" TIMESTAMPTZ,
547 "revoked_by_member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
548 "suggested_initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
549 "admitted" BOOLEAN,
550 "supporter_count" INT4,
551 "informed_supporter_count" INT4,
552 "satisfied_supporter_count" INT4,
553 "satisfied_informed_supporter_count" INT4,
554 "positive_votes" INT4,
555 "negative_votes" INT4,
556 "direct_majority" BOOLEAN,
557 "indirect_majority" BOOLEAN,
558 "schulze_rank" INT4,
559 "better_than_status_quo" BOOLEAN,
560 "worse_than_status_quo" BOOLEAN,
561 "reverse_beat_path" BOOLEAN,
562 "multistage_majority" BOOLEAN,
563 "eligible" BOOLEAN,
564 "winner" BOOLEAN,
565 "rank" INT4,
566 "text_search_data" TSVECTOR,
567 CONSTRAINT "all_or_none_of_revoked_and_revoked_by_member_id_must_be_null"
568 CHECK ("revoked" NOTNULL = "revoked_by_member_id" NOTNULL),
569 CONSTRAINT "non_revoked_initiatives_cant_suggest_other"
570 CHECK ("revoked" NOTNULL OR "suggested_initiative_id" ISNULL),
571 CONSTRAINT "revoked_initiatives_cant_be_admitted"
572 CHECK ("revoked" ISNULL OR "admitted" ISNULL),
573 CONSTRAINT "non_admitted_initiatives_cant_contain_voting_results" CHECK (
574 ( "admitted" NOTNULL AND "admitted" = TRUE ) OR
575 ( "positive_votes" ISNULL AND "negative_votes" ISNULL AND
576 "direct_majority" ISNULL AND "indirect_majority" ISNULL AND
577 "schulze_rank" ISNULL AND
578 "better_than_status_quo" ISNULL AND "worse_than_status_quo" ISNULL AND
579 "reverse_beat_path" ISNULL AND "multistage_majority" ISNULL AND
580 "eligible" ISNULL AND "winner" ISNULL AND "rank" ISNULL ) ),
581 CONSTRAINT "better_excludes_worse" CHECK (NOT ("better_than_status_quo" AND "worse_than_status_quo")),
582 CONSTRAINT "minimum_requirement_to_be_eligible" CHECK (
583 "eligible" = FALSE OR
584 ("direct_majority" AND "indirect_majority" AND "better_than_status_quo") ),
585 CONSTRAINT "winner_must_be_eligible" CHECK ("winner"=FALSE OR "eligible"=TRUE),
586 CONSTRAINT "winner_must_have_first_rank" CHECK ("winner"=FALSE OR "rank"=1),
587 CONSTRAINT "eligible_at_first_rank_is_winner" CHECK ("eligible"=FALSE OR "rank"!=1 OR "winner"=TRUE),
588 CONSTRAINT "unique_rank_per_issue" UNIQUE ("issue_id", "rank") );
589 CREATE INDEX "initiative_created_idx" ON "initiative" ("created");
590 CREATE INDEX "initiative_revoked_idx" ON "initiative" ("revoked");
591 CREATE INDEX "initiative_text_search_data_idx" ON "initiative" USING gin ("text_search_data");
592 CREATE TRIGGER "update_text_search_data"
593 BEFORE INSERT OR UPDATE ON "initiative"
594 FOR EACH ROW EXECUTE PROCEDURE
595 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
596 "name", "discussion_url");
598 COMMENT ON TABLE "initiative" IS 'Group of members publishing drafts for resolutions to be passed; Frontends must ensure that initiatives of half_frozen issues are not revoked, and that initiatives of fully_frozen or closed issues are neither revoked nor created.';
600 COMMENT ON COLUMN "initiative"."discussion_url" IS 'URL pointing to a discussion platform for this initiative';
601 COMMENT ON COLUMN "initiative"."revoked" IS 'Point in time, when one initiator decided to revoke the initiative';
602 COMMENT ON COLUMN "initiative"."revoked_by_member_id" IS 'Member, who decided to revoked the initiative';
603 COMMENT ON COLUMN "initiative"."admitted" IS 'TRUE, if initiative reaches the "initiative_quorum" when freezing the issue';
604 COMMENT ON COLUMN "initiative"."supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
605 COMMENT ON COLUMN "initiative"."informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
606 COMMENT ON COLUMN "initiative"."satisfied_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
607 COMMENT ON COLUMN "initiative"."satisfied_informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
608 COMMENT ON COLUMN "initiative"."positive_votes" IS 'Calculated from table "direct_voter"';
609 COMMENT ON COLUMN "initiative"."negative_votes" IS 'Calculated from table "direct_voter"';
610 COMMENT ON COLUMN "initiative"."direct_majority" IS 'TRUE, if "positive_votes"/("positive_votes"+"negative_votes") is strictly greater or greater-equal than "direct_majority_num"/"direct_majority_den", and "positive_votes" is greater-equal than "direct_majority_positive", and ("positive_votes"+abstentions) is greater-equal than "direct_majority_non_negative"';
611 COMMENT ON COLUMN "initiative"."indirect_majority" IS 'Same as "direct_majority", but also considering indirect beat paths';
612 COMMENT ON COLUMN "initiative"."schulze_rank" IS 'Schulze-Ranking without tie-breaking';
613 COMMENT ON COLUMN "initiative"."better_than_status_quo" IS 'TRUE, if initiative has a schulze-ranking better than the status quo (without tie-breaking)';
614 COMMENT ON COLUMN "initiative"."worse_than_status_quo" IS 'TRUE, if initiative has a schulze-ranking worse than the status quo (without tie-breaking)';
615 COMMENT ON COLUMN "initiative"."reverse_beat_path" IS 'TRUE, if there is a beat path (may include ties), from this initiative to the status quo';
616 COMMENT ON COLUMN "initiative"."multistage_majority" IS 'TRUE, if either (a) this initiative has no better rank than the status quo, or (b) there exists a better ranked initiative X, which directly beats this initiative, and either more voters prefer X to this initiative than voters preferring X to the status quo or less voters prefer this initiative to X than voters preferring the status quo to X';
617 COMMENT ON COLUMN "initiative"."eligible" IS 'Initiative is "attainable" and depending on selected policy has no "reverse_beat_path" or "multistage_majority"';
618 COMMENT ON COLUMN "initiative"."winner" IS 'Winner is the "eligible" initiative with best "schulze_rank" and in case of ties with lowest "id"';
619 COMMENT ON COLUMN "initiative"."rank" IS 'Unique ranking for all "admitted" initiatives per issue; lower rank is better; a winner always has rank 1, but rank 1 does not imply that an initiative is winner; initiatives with "direct_majority" AND "indirect_majority" always have a better (lower) rank than other initiatives';
622 CREATE TABLE "battle" (
623 "issue_id" INT4 NOT NULL,
624 "winning_initiative_id" INT4,
625 FOREIGN KEY ("issue_id", "winning_initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
626 "losing_initiative_id" INT4,
627 FOREIGN KEY ("issue_id", "losing_initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
628 "count" INT4 NOT NULL,
629 CONSTRAINT "initiative_ids_not_equal" CHECK (
630 "winning_initiative_id" != "losing_initiative_id" OR
631 ( ("winning_initiative_id" NOTNULL AND "losing_initiative_id" ISNULL) OR
632 ("winning_initiative_id" ISNULL AND "losing_initiative_id" NOTNULL) ) ) );
633 CREATE UNIQUE INDEX "battle_winning_losing_idx" ON "battle" ("issue_id", "winning_initiative_id", "losing_initiative_id");
634 CREATE UNIQUE INDEX "battle_winning_null_idx" ON "battle" ("issue_id", "winning_initiative_id") WHERE "losing_initiative_id" ISNULL;
635 CREATE UNIQUE INDEX "battle_null_losing_idx" ON "battle" ("issue_id", "losing_initiative_id") WHERE "winning_initiative_id" ISNULL;
637 COMMENT ON TABLE "battle" IS 'Number of members preferring one initiative to another; Filled by "battle_view" when closing an issue; NULL as initiative_id denotes virtual "status-quo" initiative';
640 CREATE TABLE "ignored_initiative" (
641 PRIMARY KEY ("initiative_id", "member_id"),
642 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
643 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
644 CREATE INDEX "ignored_initiative_member_id_idx" ON "ignored_initiative" ("member_id");
646 COMMENT ON TABLE "ignored_initiative" IS 'Possibility to filter initiatives';
649 CREATE TABLE "initiative_setting" (
650 PRIMARY KEY ("member_id", "key", "initiative_id"),
651 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
652 "key" TEXT NOT NULL,
653 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
654 "value" TEXT NOT NULL );
656 COMMENT ON TABLE "initiative_setting" IS 'Place for frontend to store initiative specific settings of members as strings';
659 CREATE TABLE "draft" (
660 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "supporter"
661 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
662 "id" SERIAL8 PRIMARY KEY,
663 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
664 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
665 "formatting_engine" TEXT,
666 "content" TEXT NOT NULL,
667 "text_search_data" TSVECTOR );
668 CREATE INDEX "draft_created_idx" ON "draft" ("created");
669 CREATE INDEX "draft_author_id_created_idx" ON "draft" ("author_id", "created");
670 CREATE INDEX "draft_text_search_data_idx" ON "draft" USING gin ("text_search_data");
671 CREATE TRIGGER "update_text_search_data"
672 BEFORE INSERT OR UPDATE ON "draft"
673 FOR EACH ROW EXECUTE PROCEDURE
674 tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "content");
676 COMMENT ON TABLE "draft" IS 'Drafts of initiatives to solve issues; Frontends must ensure that new drafts for initiatives of half_frozen, fully_frozen or closed issues can''t be created.';
678 COMMENT ON COLUMN "draft"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used';
679 COMMENT ON COLUMN "draft"."content" IS 'Text of the draft in a format depending on the field "formatting_engine"';
682 CREATE TABLE "rendered_draft" (
683 PRIMARY KEY ("draft_id", "format"),
684 "draft_id" INT8 REFERENCES "draft" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
685 "format" TEXT,
686 "content" TEXT NOT NULL );
688 COMMENT ON TABLE "rendered_draft" IS 'This table may be used by frontends to cache "rendered" drafts (e.g. HTML output generated from wiki text)';
691 CREATE TABLE "suggestion" (
692 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "opinion"
693 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
694 "id" SERIAL8 PRIMARY KEY,
695 "draft_id" INT8 NOT NULL,
696 FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE NO ACTION ON UPDATE CASCADE,
697 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
698 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
699 "name" TEXT NOT NULL,
700 "formatting_engine" TEXT,
701 "content" TEXT NOT NULL DEFAULT '',
702 "text_search_data" TSVECTOR,
703 "minus2_unfulfilled_count" INT4,
704 "minus2_fulfilled_count" INT4,
705 "minus1_unfulfilled_count" INT4,
706 "minus1_fulfilled_count" INT4,
707 "plus1_unfulfilled_count" INT4,
708 "plus1_fulfilled_count" INT4,
709 "plus2_unfulfilled_count" INT4,
710 "plus2_fulfilled_count" INT4 );
711 CREATE INDEX "suggestion_created_idx" ON "suggestion" ("created");
712 CREATE INDEX "suggestion_author_id_created_idx" ON "suggestion" ("author_id", "created");
713 CREATE INDEX "suggestion_text_search_data_idx" ON "suggestion" USING gin ("text_search_data");
714 CREATE TRIGGER "update_text_search_data"
715 BEFORE INSERT OR UPDATE ON "suggestion"
716 FOR EACH ROW EXECUTE PROCEDURE
717 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
718 "name", "content");
720 COMMENT ON TABLE "suggestion" IS 'Suggestions to initiators, to change the current draft; must not be deleted explicitly, as they vanish automatically if the last opinion is deleted';
722 COMMENT ON COLUMN "suggestion"."draft_id" IS 'Draft, which the author has seen when composing the suggestion; should always be set by a frontend, but defaults to current draft of the initiative (implemented by trigger "default_for_draft_id")';
723 COMMENT ON COLUMN "suggestion"."minus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
724 COMMENT ON COLUMN "suggestion"."minus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
725 COMMENT ON COLUMN "suggestion"."minus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
726 COMMENT ON COLUMN "suggestion"."minus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
727 COMMENT ON COLUMN "suggestion"."plus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
728 COMMENT ON COLUMN "suggestion"."plus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
729 COMMENT ON COLUMN "suggestion"."plus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
730 COMMENT ON COLUMN "suggestion"."plus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
733 CREATE TABLE "rendered_suggestion" (
734 PRIMARY KEY ("suggestion_id", "format"),
735 "suggestion_id" INT8 REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
736 "format" TEXT,
737 "content" TEXT NOT NULL );
739 COMMENT ON TABLE "rendered_suggestion" IS 'This table may be used by frontends to cache "rendered" drafts (e.g. HTML output generated from wiki text)';
742 CREATE TABLE "suggestion_setting" (
743 PRIMARY KEY ("member_id", "key", "suggestion_id"),
744 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
745 "key" TEXT NOT NULL,
746 "suggestion_id" INT8 REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
747 "value" TEXT NOT NULL );
749 COMMENT ON TABLE "suggestion_setting" IS 'Place for frontend to store suggestion specific settings of members as strings';
752 CREATE TABLE "privilege" (
753 PRIMARY KEY ("unit_id", "member_id"),
754 "unit_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
755 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
756 "admin_manager" BOOLEAN NOT NULL DEFAULT FALSE,
757 "unit_manager" BOOLEAN NOT NULL DEFAULT FALSE,
758 "area_manager" BOOLEAN NOT NULL DEFAULT FALSE,
759 "voting_right_manager" BOOLEAN NOT NULL DEFAULT FALSE,
760 "voting_right" BOOLEAN NOT NULL DEFAULT TRUE );
762 COMMENT ON TABLE "privilege" IS 'Members rights related to each unit';
764 COMMENT ON COLUMN "privilege"."admin_manager" IS 'Grant/revoke admin privileges to/from other users';
765 COMMENT ON COLUMN "privilege"."unit_manager" IS 'Create or lock sub units';
766 COMMENT ON COLUMN "privilege"."area_manager" IS 'Create or lock areas and set area parameters';
767 COMMENT ON COLUMN "privilege"."voting_right_manager" IS 'Select which members are allowed to discuss and vote inside the unit';
768 COMMENT ON COLUMN "privilege"."voting_right" IS 'Right to discuss and vote';
771 CREATE TABLE "membership" (
772 PRIMARY KEY ("area_id", "member_id"),
773 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
774 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
775 CREATE INDEX "membership_member_id_idx" ON "membership" ("member_id");
777 COMMENT ON TABLE "membership" IS 'Interest of members in topic areas';
780 CREATE TABLE "interest" (
781 PRIMARY KEY ("issue_id", "member_id"),
782 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
783 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
784 CREATE INDEX "interest_member_id_idx" ON "interest" ("member_id");
786 COMMENT ON TABLE "interest" IS 'Interest of members in a particular issue; Frontends must ensure that interest for fully_frozen or closed issues is not added or removed.';
789 CREATE TABLE "initiator" (
790 PRIMARY KEY ("initiative_id", "member_id"),
791 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
792 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
793 "accepted" BOOLEAN );
794 CREATE INDEX "initiator_member_id_idx" ON "initiator" ("member_id");
796 COMMENT ON TABLE "initiator" IS 'Members who are allowed to post new drafts; Frontends must ensure that initiators are not added or removed from half_frozen, fully_frozen or closed initiatives.';
798 COMMENT ON COLUMN "initiator"."accepted" IS 'If "accepted" is NULL, then the member was invited to be a co-initiator, but has not answered yet. If it is TRUE, the member has accepted the invitation, if it is FALSE, the member has rejected the invitation.';
801 CREATE TABLE "supporter" (
802 "issue_id" INT4 NOT NULL,
803 PRIMARY KEY ("initiative_id", "member_id"),
804 "initiative_id" INT4,
805 "member_id" INT4,
806 "draft_id" INT8 NOT NULL,
807 FOREIGN KEY ("issue_id", "member_id") REFERENCES "interest" ("issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE,
808 FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE NO ACTION ON UPDATE CASCADE );
809 CREATE INDEX "supporter_member_id_idx" ON "supporter" ("member_id");
811 COMMENT ON TABLE "supporter" IS 'Members who support an initiative (conditionally); Frontends must ensure that supporters are not added or removed from fully_frozen or closed initiatives.';
813 COMMENT ON COLUMN "supporter"."draft_id" IS 'Latest seen draft; should always be set by a frontend, but defaults to current draft of the initiative (implemented by trigger "default_for_draft_id")';
816 CREATE TABLE "opinion" (
817 "initiative_id" INT4 NOT NULL,
818 PRIMARY KEY ("suggestion_id", "member_id"),
819 "suggestion_id" INT8,
820 "member_id" INT4,
821 "degree" INT2 NOT NULL CHECK ("degree" >= -2 AND "degree" <= 2 AND "degree" != 0),
822 "fulfilled" BOOLEAN NOT NULL DEFAULT FALSE,
823 FOREIGN KEY ("initiative_id", "suggestion_id") REFERENCES "suggestion" ("initiative_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
824 FOREIGN KEY ("initiative_id", "member_id") REFERENCES "supporter" ("initiative_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
825 CREATE INDEX "opinion_member_id_initiative_id_idx" ON "opinion" ("member_id", "initiative_id");
827 COMMENT ON TABLE "opinion" IS 'Opinion on suggestions (criticism related to initiatives); Frontends must ensure that opinions are not created modified or deleted when related to fully_frozen or closed issues.';
829 COMMENT ON COLUMN "opinion"."degree" IS '2 = fulfillment required for support; 1 = fulfillment desired; -1 = fulfillment unwanted; -2 = fulfillment cancels support';
832 CREATE TYPE "delegation_scope" AS ENUM ('unit', 'area', 'issue');
834 COMMENT ON TYPE "delegation_scope" IS 'Scope for delegations: ''unit'', ''area'', or ''issue'' (order is relevant)';
837 CREATE TABLE "delegation" (
838 "id" SERIAL8 PRIMARY KEY,
839 "truster_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
840 "trustee_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
841 "scope" "delegation_scope" NOT NULL,
842 "unit_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
843 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
844 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
845 CONSTRAINT "cant_delegate_to_yourself" CHECK ("truster_id" != "trustee_id"),
846 CONSTRAINT "no_unit_delegation_to_null"
847 CHECK ("trustee_id" NOTNULL OR "scope" != 'unit'),
848 CONSTRAINT "area_id_and_issue_id_set_according_to_scope" CHECK (
849 ("scope" = 'unit' AND "unit_id" NOTNULL AND "area_id" ISNULL AND "issue_id" ISNULL ) OR
850 ("scope" = 'area' AND "unit_id" ISNULL AND "area_id" NOTNULL AND "issue_id" ISNULL ) OR
851 ("scope" = 'issue' AND "unit_id" ISNULL AND "area_id" ISNULL AND "issue_id" NOTNULL) ),
852 UNIQUE ("unit_id", "truster_id"),
853 UNIQUE ("area_id", "truster_id"),
854 UNIQUE ("issue_id", "truster_id") );
855 CREATE INDEX "delegation_truster_id_idx" ON "delegation" ("truster_id");
856 CREATE INDEX "delegation_trustee_id_idx" ON "delegation" ("trustee_id");
858 COMMENT ON TABLE "delegation" IS 'Delegation of vote-weight to other members';
860 COMMENT ON COLUMN "delegation"."unit_id" IS 'Reference to unit, if delegation is unit-wide, otherwise NULL';
861 COMMENT ON COLUMN "delegation"."area_id" IS 'Reference to area, if delegation is area-wide, otherwise NULL';
862 COMMENT ON COLUMN "delegation"."issue_id" IS 'Reference to issue, if delegation is issue-wide, otherwise NULL';
865 CREATE TABLE "direct_population_snapshot" (
866 PRIMARY KEY ("issue_id", "event", "member_id"),
867 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
868 "event" "snapshot_event",
869 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
870 "weight" INT4 );
871 CREATE INDEX "direct_population_snapshot_member_id_idx" ON "direct_population_snapshot" ("member_id");
873 COMMENT ON TABLE "direct_population_snapshot" IS 'Snapshot of active members having either a "membership" in the "area" or an "interest" in the "issue"';
875 COMMENT ON COLUMN "direct_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
876 COMMENT ON COLUMN "direct_population_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_population_snapshot"';
879 CREATE TABLE "delegating_population_snapshot" (
880 PRIMARY KEY ("issue_id", "event", "member_id"),
881 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
882 "event" "snapshot_event",
883 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
884 "weight" INT4,
885 "scope" "delegation_scope" NOT NULL,
886 "delegate_member_ids" INT4[] NOT NULL );
887 CREATE INDEX "delegating_population_snapshot_member_id_idx" ON "delegating_population_snapshot" ("member_id");
889 COMMENT ON TABLE "direct_population_snapshot" IS 'Delegations increasing the weight of entries in the "direct_population_snapshot" table';
891 COMMENT ON COLUMN "delegating_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
892 COMMENT ON COLUMN "delegating_population_snapshot"."member_id" IS 'Delegating member';
893 COMMENT ON COLUMN "delegating_population_snapshot"."weight" IS 'Intermediate weight';
894 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"';
897 CREATE TABLE "direct_interest_snapshot" (
898 PRIMARY KEY ("issue_id", "event", "member_id"),
899 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
900 "event" "snapshot_event",
901 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
902 "weight" INT4 );
903 CREATE INDEX "direct_interest_snapshot_member_id_idx" ON "direct_interest_snapshot" ("member_id");
905 COMMENT ON TABLE "direct_interest_snapshot" IS 'Snapshot of active members having an "interest" in the "issue"';
907 COMMENT ON COLUMN "direct_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
908 COMMENT ON COLUMN "direct_interest_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_interest_snapshot"';
911 CREATE TABLE "delegating_interest_snapshot" (
912 PRIMARY KEY ("issue_id", "event", "member_id"),
913 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
914 "event" "snapshot_event",
915 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
916 "weight" INT4,
917 "scope" "delegation_scope" NOT NULL,
918 "delegate_member_ids" INT4[] NOT NULL );
919 CREATE INDEX "delegating_interest_snapshot_member_id_idx" ON "delegating_interest_snapshot" ("member_id");
921 COMMENT ON TABLE "delegating_interest_snapshot" IS 'Delegations increasing the weight of entries in the "direct_interest_snapshot" table';
923 COMMENT ON COLUMN "delegating_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
924 COMMENT ON COLUMN "delegating_interest_snapshot"."member_id" IS 'Delegating member';
925 COMMENT ON COLUMN "delegating_interest_snapshot"."weight" IS 'Intermediate weight';
926 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"';
929 CREATE TABLE "direct_supporter_snapshot" (
930 "issue_id" INT4 NOT NULL,
931 PRIMARY KEY ("initiative_id", "event", "member_id"),
932 "initiative_id" INT4,
933 "event" "snapshot_event",
934 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
935 "informed" BOOLEAN NOT NULL,
936 "satisfied" BOOLEAN NOT NULL,
937 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
938 FOREIGN KEY ("issue_id", "event", "member_id") REFERENCES "direct_interest_snapshot" ("issue_id", "event", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
939 CREATE INDEX "direct_supporter_snapshot_member_id_idx" ON "direct_supporter_snapshot" ("member_id");
941 COMMENT ON TABLE "direct_supporter_snapshot" IS 'Snapshot of supporters of initiatives (weight is stored in "direct_interest_snapshot")';
943 COMMENT ON COLUMN "direct_supporter_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
944 COMMENT ON COLUMN "direct_supporter_snapshot"."informed" IS 'Supporter has seen the latest draft of the initiative';
945 COMMENT ON COLUMN "direct_supporter_snapshot"."satisfied" IS 'Supporter has no "critical_opinion"s';
948 CREATE TABLE "non_voter" (
949 PRIMARY KEY ("issue_id", "member_id"),
950 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
951 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
952 CREATE INDEX "non_voter_member_id_idx" ON "non_voter" ("member_id");
954 COMMENT ON TABLE "non_voter" IS 'Members who decided to not vote directly on an issue';
957 CREATE TABLE "direct_voter" (
958 PRIMARY KEY ("issue_id", "member_id"),
959 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
960 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
961 "weight" INT4 );
962 CREATE INDEX "direct_voter_member_id_idx" ON "direct_voter" ("member_id");
964 COMMENT ON TABLE "direct_voter" IS 'Members having directly voted for/against initiatives of an issue; Frontends must ensure that no voters are added or removed to/from this table when the issue has been closed.';
966 COMMENT ON COLUMN "direct_voter"."weight" IS 'Weight of member (1 or higher) according to "delegating_voter" table';
969 CREATE TABLE "delegating_voter" (
970 PRIMARY KEY ("issue_id", "member_id"),
971 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
972 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
973 "weight" INT4,
974 "scope" "delegation_scope" NOT NULL,
975 "delegate_member_ids" INT4[] NOT NULL );
976 CREATE INDEX "delegating_voter_member_id_idx" ON "delegating_voter" ("member_id");
978 COMMENT ON TABLE "delegating_voter" IS 'Delegations increasing the weight of entries in the "direct_voter" table';
980 COMMENT ON COLUMN "delegating_voter"."member_id" IS 'Delegating member';
981 COMMENT ON COLUMN "delegating_voter"."weight" IS 'Intermediate weight';
982 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"';
985 CREATE TABLE "vote" (
986 "issue_id" INT4 NOT NULL,
987 PRIMARY KEY ("initiative_id", "member_id"),
988 "initiative_id" INT4,
989 "member_id" INT4,
990 "grade" INT4,
991 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
992 FOREIGN KEY ("issue_id", "member_id") REFERENCES "direct_voter" ("issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
993 CREATE INDEX "vote_member_id_idx" ON "vote" ("member_id");
995 COMMENT ON TABLE "vote" IS 'Manual and delegated votes without abstentions; Frontends must ensure that no votes are added modified or removed when the issue has been closed.';
997 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.';
1000 CREATE TABLE "issue_comment" (
1001 PRIMARY KEY ("issue_id", "member_id"),
1002 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1003 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1004 "changed" TIMESTAMPTZ NOT NULL DEFAULT now(),
1005 "formatting_engine" TEXT,
1006 "content" TEXT NOT NULL,
1007 "text_search_data" TSVECTOR );
1008 CREATE INDEX "issue_comment_member_id_idx" ON "issue_comment" ("member_id");
1009 CREATE INDEX "issue_comment_text_search_data_idx" ON "issue_comment" USING gin ("text_search_data");
1010 CREATE TRIGGER "update_text_search_data"
1011 BEFORE INSERT OR UPDATE ON "issue_comment"
1012 FOR EACH ROW EXECUTE PROCEDURE
1013 tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "content");
1015 COMMENT ON TABLE "issue_comment" IS 'Place to store free comments of members related to issues';
1017 COMMENT ON COLUMN "issue_comment"."changed" IS 'Time the comment was last changed';
1020 CREATE TABLE "rendered_issue_comment" (
1021 PRIMARY KEY ("issue_id", "member_id", "format"),
1022 FOREIGN KEY ("issue_id", "member_id")
1023 REFERENCES "issue_comment" ("issue_id", "member_id")
1024 ON DELETE CASCADE ON UPDATE CASCADE,
1025 "issue_id" INT4,
1026 "member_id" INT4,
1027 "format" TEXT,
1028 "content" TEXT NOT NULL );
1030 COMMENT ON TABLE "rendered_issue_comment" IS 'This table may be used by frontends to cache "rendered" issue comments (e.g. HTML output generated from wiki text)';
1033 CREATE TABLE "voting_comment" (
1034 PRIMARY KEY ("issue_id", "member_id"),
1035 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1036 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1037 "changed" TIMESTAMPTZ,
1038 "formatting_engine" TEXT,
1039 "content" TEXT NOT NULL,
1040 "text_search_data" TSVECTOR );
1041 CREATE INDEX "voting_comment_member_id_idx" ON "voting_comment" ("member_id");
1042 CREATE INDEX "voting_comment_text_search_data_idx" ON "voting_comment" USING gin ("text_search_data");
1043 CREATE TRIGGER "update_text_search_data"
1044 BEFORE INSERT OR UPDATE ON "voting_comment"
1045 FOR EACH ROW EXECUTE PROCEDURE
1046 tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "content");
1048 COMMENT ON TABLE "voting_comment" IS 'Storage for comments of voters to be published after voting has finished.';
1050 COMMENT ON COLUMN "voting_comment"."changed" IS 'Is to be set or updated by the frontend, if comment was inserted or updated AFTER the issue has been closed. Otherwise it shall be set to NULL.';
1053 CREATE TABLE "rendered_voting_comment" (
1054 PRIMARY KEY ("issue_id", "member_id", "format"),
1055 FOREIGN KEY ("issue_id", "member_id")
1056 REFERENCES "voting_comment" ("issue_id", "member_id")
1057 ON DELETE CASCADE ON UPDATE CASCADE,
1058 "issue_id" INT4,
1059 "member_id" INT4,
1060 "format" TEXT,
1061 "content" TEXT NOT NULL );
1063 COMMENT ON TABLE "rendered_voting_comment" IS 'This table may be used by frontends to cache "rendered" voting comments (e.g. HTML output generated from wiki text)';
1066 CREATE TYPE "event_type" AS ENUM (
1067 'issue_state_changed',
1068 'initiative_created_in_new_issue',
1069 'initiative_created_in_existing_issue',
1070 'initiative_revoked',
1071 'new_draft_created',
1072 'suggestion_created');
1074 COMMENT ON TYPE "event_type" IS 'Type used for column "event" of table "event"';
1077 CREATE TABLE "event" (
1078 "id" SERIAL8 PRIMARY KEY,
1079 "occurrence" TIMESTAMPTZ NOT NULL DEFAULT now(),
1080 "event" "event_type" NOT NULL,
1081 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
1082 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1083 "state" "issue_state" CHECK ("state" != 'calculation'),
1084 "initiative_id" INT4,
1085 "draft_id" INT8,
1086 "suggestion_id" INT8,
1087 FOREIGN KEY ("issue_id", "initiative_id")
1088 REFERENCES "initiative" ("issue_id", "id")
1089 ON DELETE CASCADE ON UPDATE CASCADE,
1090 FOREIGN KEY ("initiative_id", "draft_id")
1091 REFERENCES "draft" ("initiative_id", "id")
1092 ON DELETE CASCADE ON UPDATE CASCADE,
1093 FOREIGN KEY ("initiative_id", "suggestion_id")
1094 REFERENCES "suggestion" ("initiative_id", "id")
1095 ON DELETE CASCADE ON UPDATE CASCADE,
1096 CONSTRAINT "null_constraints_for_issue_state_changed" CHECK (
1097 "event" != 'issue_state_changed' OR (
1098 "member_id" ISNULL AND
1099 "issue_id" NOTNULL AND
1100 "state" NOTNULL AND
1101 "initiative_id" ISNULL AND
1102 "draft_id" ISNULL AND
1103 "suggestion_id" ISNULL )),
1104 CONSTRAINT "null_constraints_for_initiative_creation_or_revocation_or_new_draft" CHECK (
1105 "event" NOT IN (
1106 'initiative_created_in_new_issue',
1107 'initiative_created_in_existing_issue',
1108 'initiative_revoked',
1109 'new_draft_created'
1110 ) OR (
1111 "member_id" NOTNULL AND
1112 "issue_id" NOTNULL AND
1113 "state" NOTNULL AND
1114 "initiative_id" NOTNULL AND
1115 "draft_id" NOTNULL AND
1116 "suggestion_id" ISNULL )),
1117 CONSTRAINT "null_constraints_for_suggestion_creation" CHECK (
1118 "event" != 'suggestion_created' OR (
1119 "member_id" NOTNULL AND
1120 "issue_id" NOTNULL AND
1121 "state" NOTNULL AND
1122 "initiative_id" NOTNULL AND
1123 "draft_id" ISNULL AND
1124 "suggestion_id" NOTNULL )) );
1126 COMMENT ON TABLE "event" IS 'Event table, automatically filled by triggers';
1128 COMMENT ON COLUMN "event"."occurrence" IS 'Point in time, when event occurred';
1129 COMMENT ON COLUMN "event"."event" IS 'Type of event (see TYPE "event_type")';
1130 COMMENT ON COLUMN "event"."member_id" IS 'Member who caused the event, if applicable';
1131 COMMENT ON COLUMN "event"."state" IS 'If issue_id is set: state of affected issue; If state changed: new state';
1135 ----------------------------------------------
1136 -- Writing of history entries and event log --
1137 ----------------------------------------------
1140 CREATE FUNCTION "write_member_history_trigger"()
1141 RETURNS TRIGGER
1142 LANGUAGE 'plpgsql' VOLATILE AS $$
1143 BEGIN
1144 IF
1145 NEW."active" != OLD."active" OR
1146 NEW."name" != OLD."name"
1147 THEN
1148 INSERT INTO "member_history"
1149 ("member_id", "active", "name")
1150 VALUES (NEW."id", OLD."active", OLD."name");
1151 END IF;
1152 RETURN NULL;
1153 END;
1154 $$;
1156 CREATE TRIGGER "write_member_history"
1157 AFTER UPDATE ON "member" FOR EACH ROW EXECUTE PROCEDURE
1158 "write_member_history_trigger"();
1160 COMMENT ON FUNCTION "write_member_history_trigger"() IS 'Implementation of trigger "write_member_history" on table "member"';
1161 COMMENT ON TRIGGER "write_member_history" ON "member" IS 'When changing certain fields of a member, create a history entry in "member_history" table';
1164 CREATE FUNCTION "write_event_issue_state_changed_trigger"()
1165 RETURNS TRIGGER
1166 LANGUAGE 'plpgsql' VOLATILE AS $$
1167 BEGIN
1168 IF NEW."state" != OLD."state" AND NEW."state" != 'calculation' THEN
1169 INSERT INTO "event" ("event", "issue_id", "state")
1170 VALUES ('issue_state_changed', NEW."id", NEW."state");
1171 END IF;
1172 RETURN NULL;
1173 END;
1174 $$;
1176 CREATE TRIGGER "write_event_issue_state_changed"
1177 AFTER UPDATE ON "issue" FOR EACH ROW EXECUTE PROCEDURE
1178 "write_event_issue_state_changed_trigger"();
1180 COMMENT ON FUNCTION "write_event_issue_state_changed_trigger"() IS 'Implementation of trigger "write_event_issue_state_changed" on table "issue"';
1181 COMMENT ON TRIGGER "write_event_issue_state_changed" ON "issue" IS 'Create entry in "event" table on "state" change';
1184 CREATE FUNCTION "write_event_initiative_or_draft_created_trigger"()
1185 RETURNS TRIGGER
1186 LANGUAGE 'plpgsql' VOLATILE AS $$
1187 DECLARE
1188 "initiative_row" "initiative"%ROWTYPE;
1189 "issue_row" "issue"%ROWTYPE;
1190 "event_v" "event_type";
1191 BEGIN
1192 SELECT * INTO "initiative_row" FROM "initiative"
1193 WHERE "id" = NEW."initiative_id";
1194 SELECT * INTO "issue_row" FROM "issue"
1195 WHERE "id" = "initiative_row"."issue_id";
1196 IF EXISTS (
1197 SELECT NULL FROM "draft"
1198 WHERE "initiative_id" = NEW."initiative_id"
1199 AND "id" != NEW."id"
1200 ) THEN
1201 "event_v" := 'new_draft_created';
1202 ELSE
1203 IF EXISTS (
1204 SELECT NULL FROM "initiative"
1205 WHERE "issue_id" = "initiative_row"."issue_id"
1206 AND "id" != "initiative_row"."id"
1207 ) THEN
1208 "event_v" := 'initiative_created_in_existing_issue';
1209 ELSE
1210 "event_v" := 'initiative_created_in_new_issue';
1211 END IF;
1212 END IF;
1213 INSERT INTO "event" (
1214 "event", "member_id",
1215 "issue_id", "state", "initiative_id", "draft_id"
1216 ) VALUES (
1217 "event_v",
1218 NEW."author_id",
1219 "initiative_row"."issue_id",
1220 "issue_row"."state",
1221 "initiative_row"."id",
1222 NEW."id" );
1223 RETURN NULL;
1224 END;
1225 $$;
1227 CREATE TRIGGER "write_event_initiative_or_draft_created"
1228 AFTER INSERT ON "draft" FOR EACH ROW EXECUTE PROCEDURE
1229 "write_event_initiative_or_draft_created_trigger"();
1231 COMMENT ON FUNCTION "write_event_initiative_or_draft_created_trigger"() IS 'Implementation of trigger "write_event_initiative_or_draft_created" on table "issue"';
1232 COMMENT ON TRIGGER "write_event_initiative_or_draft_created" ON "draft" IS 'Create entry in "event" table on draft creation';
1235 CREATE FUNCTION "write_event_initiative_revoked_trigger"()
1236 RETURNS TRIGGER
1237 LANGUAGE 'plpgsql' VOLATILE AS $$
1238 DECLARE
1239 "issue_row" "issue"%ROWTYPE;
1240 BEGIN
1241 SELECT * INTO "issue_row" FROM "issue"
1242 WHERE "id" = NEW."issue_id";
1243 IF OLD."revoked" ISNULL AND NEW."revoked" NOTNULL THEN
1244 INSERT INTO "event" (
1245 "event", "member_id", "issue_id", "state", "initiative_id"
1246 ) VALUES (
1247 'initiative_revoked',
1248 NEW."revoked_by_member_id",
1249 NEW."issue_id",
1250 "issue_row"."state",
1251 NEW."id" );
1252 END IF;
1253 RETURN NULL;
1254 END;
1255 $$;
1257 CREATE TRIGGER "write_event_initiative_revoked"
1258 AFTER UPDATE ON "initiative" FOR EACH ROW EXECUTE PROCEDURE
1259 "write_event_initiative_revoked_trigger"();
1261 COMMENT ON FUNCTION "write_event_initiative_revoked_trigger"() IS 'Implementation of trigger "write_event_initiative_revoked" on table "issue"';
1262 COMMENT ON TRIGGER "write_event_initiative_revoked" ON "initiative" IS 'Create entry in "event" table, when an initiative is revoked';
1265 CREATE FUNCTION "write_event_suggestion_created_trigger"()
1266 RETURNS TRIGGER
1267 LANGUAGE 'plpgsql' VOLATILE AS $$
1268 DECLARE
1269 "initiative_row" "initiative"%ROWTYPE;
1270 "issue_row" "issue"%ROWTYPE;
1271 BEGIN
1272 SELECT * INTO "initiative_row" FROM "initiative"
1273 WHERE "id" = NEW."initiative_id";
1274 SELECT * INTO "issue_row" FROM "issue"
1275 WHERE "id" = "initiative_row"."issue_id";
1276 INSERT INTO "event" (
1277 "event", "member_id",
1278 "issue_id", "state", "initiative_id", "suggestion_id"
1279 ) VALUES (
1280 'suggestion_created',
1281 NEW."author_id",
1282 "initiative_row"."issue_id",
1283 "issue_row"."state",
1284 "initiative_row"."id",
1285 NEW."id" );
1286 RETURN NULL;
1287 END;
1288 $$;
1290 CREATE TRIGGER "write_event_suggestion_created"
1291 AFTER INSERT ON "suggestion" FOR EACH ROW EXECUTE PROCEDURE
1292 "write_event_suggestion_created_trigger"();
1294 COMMENT ON FUNCTION "write_event_suggestion_created_trigger"() IS 'Implementation of trigger "write_event_suggestion_created" on table "issue"';
1295 COMMENT ON TRIGGER "write_event_suggestion_created" ON "suggestion" IS 'Create entry in "event" table on suggestion creation';
1299 ----------------------------
1300 -- Additional constraints --
1301 ----------------------------
1304 CREATE FUNCTION "issue_requires_first_initiative_trigger"()
1305 RETURNS TRIGGER
1306 LANGUAGE 'plpgsql' VOLATILE AS $$
1307 BEGIN
1308 IF NOT EXISTS (
1309 SELECT NULL FROM "initiative" WHERE "issue_id" = NEW."id"
1310 ) THEN
1311 --RAISE 'Cannot create issue without an initial initiative.' USING
1312 -- ERRCODE = 'integrity_constraint_violation',
1313 -- HINT = 'Create issue, initiative, and draft within the same transaction.';
1314 RAISE EXCEPTION 'Cannot create issue without an initial initiative.';
1315 END IF;
1316 RETURN NULL;
1317 END;
1318 $$;
1320 CREATE CONSTRAINT TRIGGER "issue_requires_first_initiative"
1321 AFTER INSERT OR UPDATE ON "issue" DEFERRABLE INITIALLY DEFERRED
1322 FOR EACH ROW EXECUTE PROCEDURE
1323 "issue_requires_first_initiative_trigger"();
1325 COMMENT ON FUNCTION "issue_requires_first_initiative_trigger"() IS 'Implementation of trigger "issue_requires_first_initiative" on table "issue"';
1326 COMMENT ON TRIGGER "issue_requires_first_initiative" ON "issue" IS 'Ensure that new issues have at least one initiative';
1329 CREATE FUNCTION "last_initiative_deletes_issue_trigger"()
1330 RETURNS TRIGGER
1331 LANGUAGE 'plpgsql' VOLATILE AS $$
1332 DECLARE
1333 "reference_lost" BOOLEAN;
1334 BEGIN
1335 IF TG_OP = 'DELETE' THEN
1336 "reference_lost" := TRUE;
1337 ELSE
1338 "reference_lost" := NEW."issue_id" != OLD."issue_id";
1339 END IF;
1340 IF
1341 "reference_lost" AND NOT EXISTS (
1342 SELECT NULL FROM "initiative" WHERE "issue_id" = OLD."issue_id"
1343 )
1344 THEN
1345 DELETE FROM "issue" WHERE "id" = OLD."issue_id";
1346 END IF;
1347 RETURN NULL;
1348 END;
1349 $$;
1351 CREATE CONSTRAINT TRIGGER "last_initiative_deletes_issue"
1352 AFTER UPDATE OR DELETE ON "initiative" DEFERRABLE INITIALLY DEFERRED
1353 FOR EACH ROW EXECUTE PROCEDURE
1354 "last_initiative_deletes_issue_trigger"();
1356 COMMENT ON FUNCTION "last_initiative_deletes_issue_trigger"() IS 'Implementation of trigger "last_initiative_deletes_issue" on table "initiative"';
1357 COMMENT ON TRIGGER "last_initiative_deletes_issue" ON "initiative" IS 'Removing the last initiative of an issue deletes the issue';
1360 CREATE FUNCTION "initiative_requires_first_draft_trigger"()
1361 RETURNS TRIGGER
1362 LANGUAGE 'plpgsql' VOLATILE AS $$
1363 BEGIN
1364 IF NOT EXISTS (
1365 SELECT NULL FROM "draft" WHERE "initiative_id" = NEW."id"
1366 ) THEN
1367 --RAISE 'Cannot create initiative without an initial draft.' USING
1368 -- ERRCODE = 'integrity_constraint_violation',
1369 -- HINT = 'Create issue, initiative and draft within the same transaction.';
1370 RAISE EXCEPTION 'Cannot create initiative without an initial draft.';
1371 END IF;
1372 RETURN NULL;
1373 END;
1374 $$;
1376 CREATE CONSTRAINT TRIGGER "initiative_requires_first_draft"
1377 AFTER INSERT OR UPDATE ON "initiative" DEFERRABLE INITIALLY DEFERRED
1378 FOR EACH ROW EXECUTE PROCEDURE
1379 "initiative_requires_first_draft_trigger"();
1381 COMMENT ON FUNCTION "initiative_requires_first_draft_trigger"() IS 'Implementation of trigger "initiative_requires_first_draft" on table "initiative"';
1382 COMMENT ON TRIGGER "initiative_requires_first_draft" ON "initiative" IS 'Ensure that new initiatives have at least one draft';
1385 CREATE FUNCTION "last_draft_deletes_initiative_trigger"()
1386 RETURNS TRIGGER
1387 LANGUAGE 'plpgsql' VOLATILE AS $$
1388 DECLARE
1389 "reference_lost" BOOLEAN;
1390 BEGIN
1391 IF TG_OP = 'DELETE' THEN
1392 "reference_lost" := TRUE;
1393 ELSE
1394 "reference_lost" := NEW."initiative_id" != OLD."initiative_id";
1395 END IF;
1396 IF
1397 "reference_lost" AND NOT EXISTS (
1398 SELECT NULL FROM "draft" WHERE "initiative_id" = OLD."initiative_id"
1399 )
1400 THEN
1401 DELETE FROM "initiative" WHERE "id" = OLD."initiative_id";
1402 END IF;
1403 RETURN NULL;
1404 END;
1405 $$;
1407 CREATE CONSTRAINT TRIGGER "last_draft_deletes_initiative"
1408 AFTER UPDATE OR DELETE ON "draft" DEFERRABLE INITIALLY DEFERRED
1409 FOR EACH ROW EXECUTE PROCEDURE
1410 "last_draft_deletes_initiative_trigger"();
1412 COMMENT ON FUNCTION "last_draft_deletes_initiative_trigger"() IS 'Implementation of trigger "last_draft_deletes_initiative" on table "draft"';
1413 COMMENT ON TRIGGER "last_draft_deletes_initiative" ON "draft" IS 'Removing the last draft of an initiative deletes the initiative';
1416 CREATE FUNCTION "suggestion_requires_first_opinion_trigger"()
1417 RETURNS TRIGGER
1418 LANGUAGE 'plpgsql' VOLATILE AS $$
1419 BEGIN
1420 IF NOT EXISTS (
1421 SELECT NULL FROM "opinion" WHERE "suggestion_id" = NEW."id"
1422 ) THEN
1423 RAISE EXCEPTION 'Cannot create a suggestion without an opinion.';
1424 END IF;
1425 RETURN NULL;
1426 END;
1427 $$;
1429 CREATE CONSTRAINT TRIGGER "suggestion_requires_first_opinion"
1430 AFTER INSERT OR UPDATE ON "suggestion" DEFERRABLE INITIALLY DEFERRED
1431 FOR EACH ROW EXECUTE PROCEDURE
1432 "suggestion_requires_first_opinion_trigger"();
1434 COMMENT ON FUNCTION "suggestion_requires_first_opinion_trigger"() IS 'Implementation of trigger "suggestion_requires_first_opinion" on table "suggestion"';
1435 COMMENT ON TRIGGER "suggestion_requires_first_opinion" ON "suggestion" IS 'Ensure that new suggestions have at least one opinion';
1438 CREATE FUNCTION "last_opinion_deletes_suggestion_trigger"()
1439 RETURNS TRIGGER
1440 LANGUAGE 'plpgsql' VOLATILE AS $$
1441 DECLARE
1442 "reference_lost" BOOLEAN;
1443 BEGIN
1444 IF TG_OP = 'DELETE' THEN
1445 "reference_lost" := TRUE;
1446 ELSE
1447 "reference_lost" := NEW."suggestion_id" != OLD."suggestion_id";
1448 END IF;
1449 IF
1450 "reference_lost" AND NOT EXISTS (
1451 SELECT NULL FROM "opinion" WHERE "suggestion_id" = OLD."suggestion_id"
1452 )
1453 THEN
1454 DELETE FROM "suggestion" WHERE "id" = OLD."suggestion_id";
1455 END IF;
1456 RETURN NULL;
1457 END;
1458 $$;
1460 CREATE CONSTRAINT TRIGGER "last_opinion_deletes_suggestion"
1461 AFTER UPDATE OR DELETE ON "opinion" DEFERRABLE INITIALLY DEFERRED
1462 FOR EACH ROW EXECUTE PROCEDURE
1463 "last_opinion_deletes_suggestion_trigger"();
1465 COMMENT ON FUNCTION "last_opinion_deletes_suggestion_trigger"() IS 'Implementation of trigger "last_opinion_deletes_suggestion" on table "opinion"';
1466 COMMENT ON TRIGGER "last_opinion_deletes_suggestion" ON "opinion" IS 'Removing the last opinion of a suggestion deletes the suggestion';
1470 ---------------------------------------------------------------
1471 -- Ensure that votes are not modified when issues are frozen --
1472 ---------------------------------------------------------------
1474 -- NOTE: Frontends should ensure this anyway, but in case of programming
1475 -- errors the following triggers ensure data integrity.
1478 CREATE FUNCTION "forbid_changes_on_closed_issue_trigger"()
1479 RETURNS TRIGGER
1480 LANGUAGE 'plpgsql' VOLATILE AS $$
1481 DECLARE
1482 "issue_id_v" "issue"."id"%TYPE;
1483 "issue_row" "issue"%ROWTYPE;
1484 BEGIN
1485 IF TG_OP = 'DELETE' THEN
1486 "issue_id_v" := OLD."issue_id";
1487 ELSE
1488 "issue_id_v" := NEW."issue_id";
1489 END IF;
1490 SELECT INTO "issue_row" * FROM "issue"
1491 WHERE "id" = "issue_id_v" FOR SHARE;
1492 IF "issue_row"."closed" NOTNULL THEN
1493 RAISE EXCEPTION 'Tried to modify data belonging to a closed issue.';
1494 END IF;
1495 RETURN NULL;
1496 END;
1497 $$;
1499 CREATE TRIGGER "forbid_changes_on_closed_issue"
1500 AFTER INSERT OR UPDATE OR DELETE ON "direct_voter"
1501 FOR EACH ROW EXECUTE PROCEDURE
1502 "forbid_changes_on_closed_issue_trigger"();
1504 CREATE TRIGGER "forbid_changes_on_closed_issue"
1505 AFTER INSERT OR UPDATE OR DELETE ON "delegating_voter"
1506 FOR EACH ROW EXECUTE PROCEDURE
1507 "forbid_changes_on_closed_issue_trigger"();
1509 CREATE TRIGGER "forbid_changes_on_closed_issue"
1510 AFTER INSERT OR UPDATE OR DELETE ON "vote"
1511 FOR EACH ROW EXECUTE PROCEDURE
1512 "forbid_changes_on_closed_issue_trigger"();
1514 COMMENT ON FUNCTION "forbid_changes_on_closed_issue_trigger"() IS 'Implementation of triggers "forbid_changes_on_closed_issue" on tables "direct_voter", "delegating_voter" and "vote"';
1515 COMMENT ON TRIGGER "forbid_changes_on_closed_issue" ON "direct_voter" IS 'Ensures that frontends can''t tamper with votings of closed issues, in case of programming errors';
1516 COMMENT ON TRIGGER "forbid_changes_on_closed_issue" ON "delegating_voter" IS 'Ensures that frontends can''t tamper with votings of closed issues, in case of programming errors';
1517 COMMENT ON TRIGGER "forbid_changes_on_closed_issue" ON "vote" IS 'Ensures that frontends can''t tamper with votings of closed issues, in case of programming errors';
1521 --------------------------------------------------------------------
1522 -- Auto-retrieval of fields only needed for referential integrity --
1523 --------------------------------------------------------------------
1526 CREATE FUNCTION "autofill_issue_id_trigger"()
1527 RETURNS TRIGGER
1528 LANGUAGE 'plpgsql' VOLATILE AS $$
1529 BEGIN
1530 IF NEW."issue_id" ISNULL THEN
1531 SELECT "issue_id" INTO NEW."issue_id"
1532 FROM "initiative" WHERE "id" = NEW."initiative_id";
1533 END IF;
1534 RETURN NEW;
1535 END;
1536 $$;
1538 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "supporter"
1539 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
1541 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "vote"
1542 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
1544 COMMENT ON FUNCTION "autofill_issue_id_trigger"() IS 'Implementation of triggers "autofill_issue_id" on tables "supporter" and "vote"';
1545 COMMENT ON TRIGGER "autofill_issue_id" ON "supporter" IS 'Set "issue_id" field automatically, if NULL';
1546 COMMENT ON TRIGGER "autofill_issue_id" ON "vote" IS 'Set "issue_id" field automatically, if NULL';
1549 CREATE FUNCTION "autofill_initiative_id_trigger"()
1550 RETURNS TRIGGER
1551 LANGUAGE 'plpgsql' VOLATILE AS $$
1552 BEGIN
1553 IF NEW."initiative_id" ISNULL THEN
1554 SELECT "initiative_id" INTO NEW."initiative_id"
1555 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
1556 END IF;
1557 RETURN NEW;
1558 END;
1559 $$;
1561 CREATE TRIGGER "autofill_initiative_id" BEFORE INSERT ON "opinion"
1562 FOR EACH ROW EXECUTE PROCEDURE "autofill_initiative_id_trigger"();
1564 COMMENT ON FUNCTION "autofill_initiative_id_trigger"() IS 'Implementation of trigger "autofill_initiative_id" on table "opinion"';
1565 COMMENT ON TRIGGER "autofill_initiative_id" ON "opinion" IS 'Set "initiative_id" field automatically, if NULL';
1569 -----------------------------------------------------
1570 -- Automatic calculation of certain default values --
1571 -----------------------------------------------------
1574 CREATE FUNCTION "copy_timings_trigger"()
1575 RETURNS TRIGGER
1576 LANGUAGE 'plpgsql' VOLATILE AS $$
1577 DECLARE
1578 "policy_row" "policy"%ROWTYPE;
1579 BEGIN
1580 SELECT * INTO "policy_row" FROM "policy"
1581 WHERE "id" = NEW."policy_id";
1582 IF NEW."admission_time" ISNULL THEN
1583 NEW."admission_time" := "policy_row"."admission_time";
1584 END IF;
1585 IF NEW."discussion_time" ISNULL THEN
1586 NEW."discussion_time" := "policy_row"."discussion_time";
1587 END IF;
1588 IF NEW."verification_time" ISNULL THEN
1589 NEW."verification_time" := "policy_row"."verification_time";
1590 END IF;
1591 IF NEW."voting_time" ISNULL THEN
1592 NEW."voting_time" := "policy_row"."voting_time";
1593 END IF;
1594 RETURN NEW;
1595 END;
1596 $$;
1598 CREATE TRIGGER "copy_timings" BEFORE INSERT OR UPDATE ON "issue"
1599 FOR EACH ROW EXECUTE PROCEDURE "copy_timings_trigger"();
1601 COMMENT ON FUNCTION "copy_timings_trigger"() IS 'Implementation of trigger "copy_timings" on table "issue"';
1602 COMMENT ON TRIGGER "copy_timings" ON "issue" IS 'If timing fields are NULL, copy values from policy.';
1605 CREATE FUNCTION "default_for_draft_id_trigger"()
1606 RETURNS TRIGGER
1607 LANGUAGE 'plpgsql' VOLATILE AS $$
1608 BEGIN
1609 IF NEW."draft_id" ISNULL THEN
1610 SELECT "id" INTO NEW."draft_id" FROM "current_draft"
1611 WHERE "initiative_id" = NEW."initiative_id";
1612 END IF;
1613 RETURN NEW;
1614 END;
1615 $$;
1617 CREATE TRIGGER "default_for_draft_id" BEFORE INSERT OR UPDATE ON "suggestion"
1618 FOR EACH ROW EXECUTE PROCEDURE "default_for_draft_id_trigger"();
1619 CREATE TRIGGER "default_for_draft_id" BEFORE INSERT OR UPDATE ON "supporter"
1620 FOR EACH ROW EXECUTE PROCEDURE "default_for_draft_id_trigger"();
1622 COMMENT ON FUNCTION "default_for_draft_id_trigger"() IS 'Implementation of trigger "default_for_draft" on tables "supporter" and "suggestion"';
1623 COMMENT ON TRIGGER "default_for_draft_id" ON "suggestion" IS 'If "draft_id" is NULL, then use the current draft of the initiative as default';
1624 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';
1628 ----------------------------------------
1629 -- Automatic creation of dependencies --
1630 ----------------------------------------
1633 CREATE FUNCTION "autocreate_interest_trigger"()
1634 RETURNS TRIGGER
1635 LANGUAGE 'plpgsql' VOLATILE AS $$
1636 BEGIN
1637 IF NOT EXISTS (
1638 SELECT NULL FROM "initiative" JOIN "interest"
1639 ON "initiative"."issue_id" = "interest"."issue_id"
1640 WHERE "initiative"."id" = NEW."initiative_id"
1641 AND "interest"."member_id" = NEW."member_id"
1642 ) THEN
1643 BEGIN
1644 INSERT INTO "interest" ("issue_id", "member_id")
1645 SELECT "issue_id", NEW."member_id"
1646 FROM "initiative" WHERE "id" = NEW."initiative_id";
1647 EXCEPTION WHEN unique_violation THEN END;
1648 END IF;
1649 RETURN NEW;
1650 END;
1651 $$;
1653 CREATE TRIGGER "autocreate_interest" BEFORE INSERT ON "supporter"
1654 FOR EACH ROW EXECUTE PROCEDURE "autocreate_interest_trigger"();
1656 COMMENT ON FUNCTION "autocreate_interest_trigger"() IS 'Implementation of trigger "autocreate_interest" on table "supporter"';
1657 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';
1660 CREATE FUNCTION "autocreate_supporter_trigger"()
1661 RETURNS TRIGGER
1662 LANGUAGE 'plpgsql' VOLATILE AS $$
1663 BEGIN
1664 IF NOT EXISTS (
1665 SELECT NULL FROM "suggestion" JOIN "supporter"
1666 ON "suggestion"."initiative_id" = "supporter"."initiative_id"
1667 WHERE "suggestion"."id" = NEW."suggestion_id"
1668 AND "supporter"."member_id" = NEW."member_id"
1669 ) THEN
1670 BEGIN
1671 INSERT INTO "supporter" ("initiative_id", "member_id")
1672 SELECT "initiative_id", NEW."member_id"
1673 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
1674 EXCEPTION WHEN unique_violation THEN END;
1675 END IF;
1676 RETURN NEW;
1677 END;
1678 $$;
1680 CREATE TRIGGER "autocreate_supporter" BEFORE INSERT ON "opinion"
1681 FOR EACH ROW EXECUTE PROCEDURE "autocreate_supporter_trigger"();
1683 COMMENT ON FUNCTION "autocreate_supporter_trigger"() IS 'Implementation of trigger "autocreate_supporter" on table "opinion"';
1684 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.';
1688 ------------------------------------------
1689 -- Views and helper functions for views --
1690 ------------------------------------------
1693 CREATE VIEW "unit_delegation" AS
1694 SELECT
1695 "unit"."id" AS "unit_id",
1696 "delegation"."id",
1697 "delegation"."truster_id",
1698 "delegation"."trustee_id",
1699 "delegation"."scope"
1700 FROM "unit"
1701 JOIN "delegation"
1702 ON "delegation"."unit_id" = "unit"."id"
1703 JOIN "member"
1704 ON "delegation"."truster_id" = "member"."id"
1705 JOIN "privilege"
1706 ON "delegation"."unit_id" = "privilege"."unit_id"
1707 AND "delegation"."truster_id" = "privilege"."member_id"
1708 WHERE "member"."active" AND "privilege"."voting_right";
1710 COMMENT ON VIEW "unit_delegation" IS 'Unit delegations where trusters are active and have voting right';
1713 CREATE VIEW "area_delegation" AS
1714 SELECT DISTINCT ON ("area"."id", "delegation"."truster_id")
1715 "area"."id" AS "area_id",
1716 "delegation"."id",
1717 "delegation"."truster_id",
1718 "delegation"."trustee_id",
1719 "delegation"."scope"
1720 FROM "area"
1721 JOIN "delegation"
1722 ON "delegation"."unit_id" = "area"."unit_id"
1723 OR "delegation"."area_id" = "area"."id"
1724 JOIN "member"
1725 ON "delegation"."truster_id" = "member"."id"
1726 JOIN "privilege"
1727 ON "area"."unit_id" = "privilege"."unit_id"
1728 AND "delegation"."truster_id" = "privilege"."member_id"
1729 WHERE "member"."active" AND "privilege"."voting_right"
1730 ORDER BY
1731 "area"."id",
1732 "delegation"."truster_id",
1733 "delegation"."scope" DESC;
1735 COMMENT ON VIEW "area_delegation" IS 'Area delegations where trusters are active and have voting right';
1738 CREATE VIEW "issue_delegation" AS
1739 SELECT DISTINCT ON ("issue"."id", "delegation"."truster_id")
1740 "issue"."id" AS "issue_id",
1741 "delegation"."id",
1742 "delegation"."truster_id",
1743 "delegation"."trustee_id",
1744 "delegation"."scope"
1745 FROM "issue"
1746 JOIN "area"
1747 ON "area"."id" = "issue"."area_id"
1748 JOIN "delegation"
1749 ON "delegation"."unit_id" = "area"."unit_id"
1750 OR "delegation"."area_id" = "area"."id"
1751 OR "delegation"."issue_id" = "issue"."id"
1752 JOIN "member"
1753 ON "delegation"."truster_id" = "member"."id"
1754 JOIN "privilege"
1755 ON "area"."unit_id" = "privilege"."unit_id"
1756 AND "delegation"."truster_id" = "privilege"."member_id"
1757 WHERE "member"."active" AND "privilege"."voting_right"
1758 ORDER BY
1759 "issue"."id",
1760 "delegation"."truster_id",
1761 "delegation"."scope" DESC;
1763 COMMENT ON VIEW "issue_delegation" IS 'Issue delegations where trusters are active and have voting right';
1766 CREATE FUNCTION "membership_weight_with_skipping"
1767 ( "area_id_p" "area"."id"%TYPE,
1768 "member_id_p" "member"."id"%TYPE,
1769 "skip_member_ids_p" INT4[] ) -- "member"."id"%TYPE[]
1770 RETURNS INT4
1771 LANGUAGE 'plpgsql' STABLE AS $$
1772 DECLARE
1773 "sum_v" INT4;
1774 "delegation_row" "area_delegation"%ROWTYPE;
1775 BEGIN
1776 "sum_v" := 1;
1777 FOR "delegation_row" IN
1778 SELECT "area_delegation".*
1779 FROM "area_delegation" LEFT JOIN "membership"
1780 ON "membership"."area_id" = "area_id_p"
1781 AND "membership"."member_id" = "area_delegation"."truster_id"
1782 WHERE "area_delegation"."area_id" = "area_id_p"
1783 AND "area_delegation"."trustee_id" = "member_id_p"
1784 AND "membership"."member_id" ISNULL
1785 LOOP
1786 IF NOT
1787 "skip_member_ids_p" @> ARRAY["delegation_row"."truster_id"]
1788 THEN
1789 "sum_v" := "sum_v" + "membership_weight_with_skipping"(
1790 "area_id_p",
1791 "delegation_row"."truster_id",
1792 "skip_member_ids_p" || "delegation_row"."truster_id"
1793 );
1794 END IF;
1795 END LOOP;
1796 RETURN "sum_v";
1797 END;
1798 $$;
1800 COMMENT ON FUNCTION "membership_weight_with_skipping"
1801 ( "area"."id"%TYPE,
1802 "member"."id"%TYPE,
1803 INT4[] )
1804 IS 'Helper function for "membership_weight" function';
1807 CREATE FUNCTION "membership_weight"
1808 ( "area_id_p" "area"."id"%TYPE,
1809 "member_id_p" "member"."id"%TYPE ) -- "member"."id"%TYPE[]
1810 RETURNS INT4
1811 LANGUAGE 'plpgsql' STABLE AS $$
1812 BEGIN
1813 RETURN "membership_weight_with_skipping"(
1814 "area_id_p",
1815 "member_id_p",
1816 ARRAY["member_id_p"]
1817 );
1818 END;
1819 $$;
1821 COMMENT ON FUNCTION "membership_weight"
1822 ( "area"."id"%TYPE,
1823 "member"."id"%TYPE )
1824 IS 'Calculates the potential voting weight of a member in a given area';
1827 CREATE VIEW "member_count_view" AS
1828 SELECT count(1) AS "total_count" FROM "member" WHERE "active";
1830 COMMENT ON VIEW "member_count_view" IS 'View used to update "member_count" table';
1833 CREATE VIEW "unit_member_count" AS
1834 SELECT
1835 "unit"."id" AS "unit_id",
1836 sum("member"."id") AS "member_count"
1837 FROM "unit"
1838 LEFT JOIN "privilege"
1839 ON "privilege"."unit_id" = "unit"."id"
1840 AND "privilege"."voting_right"
1841 LEFT JOIN "member"
1842 ON "member"."id" = "privilege"."member_id"
1843 AND "member"."active"
1844 GROUP BY "unit"."id";
1846 COMMENT ON VIEW "unit_member_count" IS 'View used to update "member_count" column of "unit" table';
1849 CREATE VIEW "area_member_count" AS
1850 SELECT
1851 "area"."id" AS "area_id",
1852 count("member"."id") AS "direct_member_count",
1853 coalesce(
1854 sum(
1855 CASE WHEN "member"."id" NOTNULL THEN
1856 "membership_weight"("area"."id", "member"."id")
1857 ELSE 0 END
1858 )
1859 ) AS "member_weight"
1860 FROM "area"
1861 LEFT JOIN "membership"
1862 ON "area"."id" = "membership"."area_id"
1863 LEFT JOIN "privilege"
1864 ON "privilege"."unit_id" = "area"."unit_id"
1865 AND "privilege"."member_id" = "membership"."member_id"
1866 AND "privilege"."voting_right"
1867 LEFT JOIN "member"
1868 ON "member"."id" = "privilege"."member_id" -- NOTE: no membership here!
1869 AND "member"."active"
1870 GROUP BY "area"."id";
1872 COMMENT ON VIEW "area_member_count" IS 'View used to update "direct_member_count" and "member_weight" columns of table "area"';
1875 CREATE VIEW "opening_draft" AS
1876 SELECT "draft".* FROM (
1877 SELECT
1878 "initiative"."id" AS "initiative_id",
1879 min("draft"."id") AS "draft_id"
1880 FROM "initiative" JOIN "draft"
1881 ON "initiative"."id" = "draft"."initiative_id"
1882 GROUP BY "initiative"."id"
1883 ) AS "subquery"
1884 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
1886 COMMENT ON VIEW "opening_draft" IS 'First drafts of all initiatives';
1889 CREATE VIEW "current_draft" AS
1890 SELECT "draft".* FROM (
1891 SELECT
1892 "initiative"."id" AS "initiative_id",
1893 max("draft"."id") AS "draft_id"
1894 FROM "initiative" JOIN "draft"
1895 ON "initiative"."id" = "draft"."initiative_id"
1896 GROUP BY "initiative"."id"
1897 ) AS "subquery"
1898 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
1900 COMMENT ON VIEW "current_draft" IS 'All latest drafts for each initiative';
1903 CREATE VIEW "critical_opinion" AS
1904 SELECT * FROM "opinion"
1905 WHERE ("degree" = 2 AND "fulfilled" = FALSE)
1906 OR ("degree" = -2 AND "fulfilled" = TRUE);
1908 COMMENT ON VIEW "critical_opinion" IS 'Opinions currently causing dissatisfaction';
1911 CREATE VIEW "battle_participant" AS
1912 SELECT "initiative"."id", "initiative"."issue_id"
1913 FROM "issue" JOIN "initiative"
1914 ON "issue"."id" = "initiative"."issue_id"
1915 WHERE "initiative"."admitted"
1916 UNION ALL
1917 SELECT NULL, "id" AS "issue_id"
1918 FROM "issue";
1920 COMMENT ON VIEW "battle_participant" IS 'Helper view for "battle_view" containing admitted initiatives plus virtual "status-quo" initiative denoted by NULL reference';
1923 CREATE VIEW "battle_view" AS
1924 SELECT
1925 "issue"."id" AS "issue_id",
1926 "winning_initiative"."id" AS "winning_initiative_id",
1927 "losing_initiative"."id" AS "losing_initiative_id",
1928 sum(
1929 CASE WHEN
1930 coalesce("better_vote"."grade", 0) >
1931 coalesce("worse_vote"."grade", 0)
1932 THEN "direct_voter"."weight" ELSE 0 END
1933 ) AS "count"
1934 FROM "issue"
1935 LEFT JOIN "direct_voter"
1936 ON "issue"."id" = "direct_voter"."issue_id"
1937 JOIN "battle_participant" AS "winning_initiative"
1938 ON "issue"."id" = "winning_initiative"."issue_id"
1939 JOIN "battle_participant" AS "losing_initiative"
1940 ON "issue"."id" = "losing_initiative"."issue_id"
1941 LEFT JOIN "vote" AS "better_vote"
1942 ON "direct_voter"."member_id" = "better_vote"."member_id"
1943 AND "winning_initiative"."id" = "better_vote"."initiative_id"
1944 LEFT JOIN "vote" AS "worse_vote"
1945 ON "direct_voter"."member_id" = "worse_vote"."member_id"
1946 AND "losing_initiative"."id" = "worse_vote"."initiative_id"
1947 WHERE "issue"."closed" NOTNULL
1948 AND "issue"."cleaned" ISNULL
1949 AND (
1950 "winning_initiative"."id" != "losing_initiative"."id" OR
1951 ( ("winning_initiative"."id" NOTNULL AND "losing_initiative"."id" ISNULL) OR
1952 ("winning_initiative"."id" ISNULL AND "losing_initiative"."id" NOTNULL) ) )
1953 GROUP BY
1954 "issue"."id",
1955 "winning_initiative"."id",
1956 "losing_initiative"."id";
1958 COMMENT ON VIEW "battle_view" IS 'Number of members preferring one initiative (or status-quo) to another initiative (or status-quo); Used to fill "battle" table';
1961 CREATE VIEW "expired_session" AS
1962 SELECT * FROM "session" WHERE now() > "expiry";
1964 CREATE RULE "delete" AS ON DELETE TO "expired_session" DO INSTEAD
1965 DELETE FROM "session" WHERE "ident" = OLD."ident";
1967 COMMENT ON VIEW "expired_session" IS 'View containing all expired sessions where DELETE is possible';
1968 COMMENT ON RULE "delete" ON "expired_session" IS 'Rule allowing DELETE on rows in "expired_session" view, i.e. DELETE FROM "expired_session"';
1971 CREATE VIEW "open_issue" AS
1972 SELECT * FROM "issue" WHERE "closed" ISNULL;
1974 COMMENT ON VIEW "open_issue" IS 'All open issues';
1977 CREATE VIEW "issue_with_ranks_missing" AS
1978 SELECT * FROM "issue"
1979 WHERE "fully_frozen" NOTNULL
1980 AND "closed" NOTNULL
1981 AND "ranks_available" = FALSE;
1983 COMMENT ON VIEW "issue_with_ranks_missing" IS 'Issues where voting was finished, but no ranks have been calculated yet';
1986 CREATE VIEW "member_contingent" AS
1987 SELECT
1988 "member"."id" AS "member_id",
1989 "contingent"."time_frame",
1990 CASE WHEN "contingent"."text_entry_limit" NOTNULL THEN
1991 (
1992 SELECT count(1) FROM "draft"
1993 WHERE "draft"."author_id" = "member"."id"
1994 AND "draft"."created" > now() - "contingent"."time_frame"
1995 ) + (
1996 SELECT count(1) FROM "suggestion"
1997 WHERE "suggestion"."author_id" = "member"."id"
1998 AND "suggestion"."created" > now() - "contingent"."time_frame"
1999 )
2000 ELSE NULL END AS "text_entry_count",
2001 "contingent"."text_entry_limit",
2002 CASE WHEN "contingent"."initiative_limit" NOTNULL THEN (
2003 SELECT count(1) FROM "opening_draft"
2004 WHERE "opening_draft"."author_id" = "member"."id"
2005 AND "opening_draft"."created" > now() - "contingent"."time_frame"
2006 ) ELSE NULL END AS "initiative_count",
2007 "contingent"."initiative_limit"
2008 FROM "member" CROSS JOIN "contingent";
2010 COMMENT ON VIEW "member_contingent" IS 'Actual counts of text entries and initiatives are calculated per member for each limit in the "contingent" table.';
2012 COMMENT ON COLUMN "member_contingent"."text_entry_count" IS 'Only calculated when "text_entry_limit" is not null in the same row';
2013 COMMENT ON COLUMN "member_contingent"."initiative_count" IS 'Only calculated when "initiative_limit" is not null in the same row';
2016 CREATE VIEW "member_contingent_left" AS
2017 SELECT
2018 "member_id",
2019 max("text_entry_limit" - "text_entry_count") AS "text_entries_left",
2020 max("initiative_limit" - "initiative_count") AS "initiatives_left"
2021 FROM "member_contingent" GROUP BY "member_id";
2023 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.';
2026 CREATE VIEW "event_seen_by_member" AS
2027 SELECT
2028 "member"."id" AS "seen_by_member_id",
2029 CASE WHEN "event"."state" IN (
2030 'voting',
2031 'finished_without_winner',
2032 'finished_with_winner'
2033 ) THEN
2034 'voting'::"notify_level"
2035 ELSE
2036 CASE WHEN "event"."state" IN (
2037 'verification',
2038 'canceled_after_revocation_during_verification',
2039 'canceled_no_initiative_admitted'
2040 ) THEN
2041 'verification'::"notify_level"
2042 ELSE
2043 CASE WHEN "event"."state" IN (
2044 'discussion',
2045 'canceled_after_revocation_during_discussion'
2046 ) THEN
2047 'discussion'::"notify_level"
2048 ELSE
2049 'all'::"notify_level"
2050 END
2051 END
2052 END AS "notify_level",
2053 "event".*
2054 FROM "member" CROSS JOIN "event"
2055 LEFT JOIN "issue"
2056 ON "event"."issue_id" = "issue"."id"
2057 LEFT JOIN "membership"
2058 ON "member"."id" = "membership"."member_id"
2059 AND "issue"."area_id" = "membership"."area_id"
2060 LEFT JOIN "interest"
2061 ON "member"."id" = "interest"."member_id"
2062 AND "event"."issue_id" = "interest"."issue_id"
2063 LEFT JOIN "supporter"
2064 ON "member"."id" = "supporter"."member_id"
2065 AND "event"."initiative_id" = "supporter"."initiative_id"
2066 LEFT JOIN "ignored_member"
2067 ON "member"."id" = "ignored_member"."member_id"
2068 AND "event"."member_id" = "ignored_member"."other_member_id"
2069 LEFT JOIN "ignored_initiative"
2070 ON "member"."id" = "ignored_initiative"."member_id"
2071 AND "event"."initiative_id" = "ignored_initiative"."initiative_id"
2072 WHERE (
2073 "supporter"."member_id" NOTNULL OR
2074 "interest"."member_id" NOTNULL OR
2075 ( "membership"."member_id" NOTNULL AND
2076 "event"."event" IN (
2077 'issue_state_changed',
2078 'initiative_created_in_new_issue',
2079 'initiative_created_in_existing_issue',
2080 'initiative_revoked' ) ) )
2081 AND "ignored_member"."member_id" ISNULL
2082 AND "ignored_initiative"."member_id" ISNULL;
2084 COMMENT ON VIEW "event_seen_by_member" IS 'Events as seen by a member, depending on its memberships, interests and support';
2087 CREATE VIEW "pending_notification" AS
2088 SELECT
2089 "member"."id" AS "seen_by_member_id",
2090 "event".*
2091 FROM "member" CROSS JOIN "event"
2092 LEFT JOIN "issue"
2093 ON "event"."issue_id" = "issue"."id"
2094 LEFT JOIN "membership"
2095 ON "member"."id" = "membership"."member_id"
2096 AND "issue"."area_id" = "membership"."area_id"
2097 LEFT JOIN "interest"
2098 ON "member"."id" = "interest"."member_id"
2099 AND "event"."issue_id" = "interest"."issue_id"
2100 LEFT JOIN "supporter"
2101 ON "member"."id" = "supporter"."member_id"
2102 AND "event"."initiative_id" = "supporter"."initiative_id"
2103 LEFT JOIN "ignored_member"
2104 ON "member"."id" = "ignored_member"."member_id"
2105 AND "event"."member_id" = "ignored_member"."other_member_id"
2106 LEFT JOIN "ignored_initiative"
2107 ON "member"."id" = "ignored_initiative"."member_id"
2108 AND "event"."initiative_id" = "ignored_initiative"."initiative_id"
2109 WHERE (
2110 "member"."notify_event_id" ISNULL OR
2111 ( "member"."notify_event_id" NOTNULL AND
2112 "member"."notify_event_id" < "event"."id" ) )
2113 AND (
2114 ( "member"."notify_level" >= 'all' ) OR
2115 ( "member"."notify_level" >= 'voting' AND
2116 "event"."state" IN (
2117 'voting',
2118 'finished_without_winner',
2119 'finished_with_winner' ) ) OR
2120 ( "member"."notify_level" >= 'verification' AND
2121 "event"."state" IN (
2122 'verification',
2123 'canceled_after_revocation_during_verification',
2124 'canceled_no_initiative_admitted' ) ) OR
2125 ( "member"."notify_level" >= 'discussion' AND
2126 "event"."state" IN (
2127 'discussion',
2128 'canceled_after_revocation_during_discussion' ) ) )
2129 AND (
2130 "supporter"."member_id" NOTNULL OR
2131 "interest"."member_id" NOTNULL OR
2132 ( "membership"."member_id" NOTNULL AND
2133 "event"."event" IN (
2134 'issue_state_changed',
2135 'initiative_created_in_new_issue',
2136 'initiative_created_in_existing_issue',
2137 'initiative_revoked' ) ) )
2138 AND "ignored_member"."member_id" ISNULL
2139 AND "ignored_initiative"."member_id" ISNULL;
2141 COMMENT ON VIEW "pending_notification" IS 'Events to be sent to "notify_email" address of member referred to by "seen_by_member_id"';
2144 CREATE TYPE "timeline_event" AS ENUM (
2145 'issue_created',
2146 'issue_canceled',
2147 'issue_accepted',
2148 'issue_half_frozen',
2149 'issue_finished_without_voting',
2150 'issue_voting_started',
2151 'issue_finished_after_voting',
2152 'initiative_created',
2153 'initiative_revoked',
2154 'draft_created',
2155 'suggestion_created');
2157 COMMENT ON TYPE "timeline_event" IS 'Types of event in timeline tables (DEPRECATED)';
2160 CREATE VIEW "timeline_issue" AS
2161 SELECT
2162 "created" AS "occurrence",
2163 'issue_created'::"timeline_event" AS "event",
2164 "id" AS "issue_id"
2165 FROM "issue"
2166 UNION ALL
2167 SELECT
2168 "closed" AS "occurrence",
2169 'issue_canceled'::"timeline_event" AS "event",
2170 "id" AS "issue_id"
2171 FROM "issue" WHERE "closed" NOTNULL AND "fully_frozen" ISNULL
2172 UNION ALL
2173 SELECT
2174 "accepted" AS "occurrence",
2175 'issue_accepted'::"timeline_event" AS "event",
2176 "id" AS "issue_id"
2177 FROM "issue" WHERE "accepted" NOTNULL
2178 UNION ALL
2179 SELECT
2180 "half_frozen" AS "occurrence",
2181 'issue_half_frozen'::"timeline_event" AS "event",
2182 "id" AS "issue_id"
2183 FROM "issue" WHERE "half_frozen" NOTNULL
2184 UNION ALL
2185 SELECT
2186 "fully_frozen" AS "occurrence",
2187 'issue_voting_started'::"timeline_event" AS "event",
2188 "id" AS "issue_id"
2189 FROM "issue"
2190 WHERE "fully_frozen" NOTNULL
2191 AND ("closed" ISNULL OR "closed" != "fully_frozen")
2192 UNION ALL
2193 SELECT
2194 "closed" AS "occurrence",
2195 CASE WHEN "fully_frozen" = "closed" THEN
2196 'issue_finished_without_voting'::"timeline_event"
2197 ELSE
2198 'issue_finished_after_voting'::"timeline_event"
2199 END AS "event",
2200 "id" AS "issue_id"
2201 FROM "issue" WHERE "closed" NOTNULL AND "fully_frozen" NOTNULL;
2203 COMMENT ON VIEW "timeline_issue" IS 'Helper view for "timeline" view (DEPRECATED)';
2206 CREATE VIEW "timeline_initiative" AS
2207 SELECT
2208 "created" AS "occurrence",
2209 'initiative_created'::"timeline_event" AS "event",
2210 "id" AS "initiative_id"
2211 FROM "initiative"
2212 UNION ALL
2213 SELECT
2214 "revoked" AS "occurrence",
2215 'initiative_revoked'::"timeline_event" AS "event",
2216 "id" AS "initiative_id"
2217 FROM "initiative" WHERE "revoked" NOTNULL;
2219 COMMENT ON VIEW "timeline_initiative" IS 'Helper view for "timeline" view (DEPRECATED)';
2222 CREATE VIEW "timeline_draft" AS
2223 SELECT
2224 "created" AS "occurrence",
2225 'draft_created'::"timeline_event" AS "event",
2226 "id" AS "draft_id"
2227 FROM "draft";
2229 COMMENT ON VIEW "timeline_draft" IS 'Helper view for "timeline" view (DEPRECATED)';
2232 CREATE VIEW "timeline_suggestion" AS
2233 SELECT
2234 "created" AS "occurrence",
2235 'suggestion_created'::"timeline_event" AS "event",
2236 "id" AS "suggestion_id"
2237 FROM "suggestion";
2239 COMMENT ON VIEW "timeline_suggestion" IS 'Helper view for "timeline" view (DEPRECATED)';
2242 CREATE VIEW "timeline" AS
2243 SELECT
2244 "occurrence",
2245 "event",
2246 "issue_id",
2247 NULL AS "initiative_id",
2248 NULL::INT8 AS "draft_id", -- TODO: Why do we need a type-cast here? Is this due to 32 bit architecture?
2249 NULL::INT8 AS "suggestion_id"
2250 FROM "timeline_issue"
2251 UNION ALL
2252 SELECT
2253 "occurrence",
2254 "event",
2255 NULL AS "issue_id",
2256 "initiative_id",
2257 NULL AS "draft_id",
2258 NULL AS "suggestion_id"
2259 FROM "timeline_initiative"
2260 UNION ALL
2261 SELECT
2262 "occurrence",
2263 "event",
2264 NULL AS "issue_id",
2265 NULL AS "initiative_id",
2266 "draft_id",
2267 NULL AS "suggestion_id"
2268 FROM "timeline_draft"
2269 UNION ALL
2270 SELECT
2271 "occurrence",
2272 "event",
2273 NULL AS "issue_id",
2274 NULL AS "initiative_id",
2275 NULL AS "draft_id",
2276 "suggestion_id"
2277 FROM "timeline_suggestion";
2279 COMMENT ON VIEW "timeline" IS 'Aggregation of different events in the system (DEPRECATED)';
2283 --------------------------------------------------
2284 -- Set returning function for delegation chains --
2285 --------------------------------------------------
2288 CREATE TYPE "delegation_chain_loop_tag" AS ENUM
2289 ('first', 'intermediate', 'last', 'repetition');
2291 COMMENT ON TYPE "delegation_chain_loop_tag" IS 'Type for loop tags in "delegation_chain_row" type';
2294 CREATE TYPE "delegation_chain_row" AS (
2295 "index" INT4,
2296 "member_id" INT4,
2297 "member_valid" BOOLEAN,
2298 "participation" BOOLEAN,
2299 "overridden" BOOLEAN,
2300 "scope_in" "delegation_scope",
2301 "scope_out" "delegation_scope",
2302 "disabled_out" BOOLEAN,
2303 "loop" "delegation_chain_loop_tag" );
2305 COMMENT ON TYPE "delegation_chain_row" IS 'Type of rows returned by "delegation_chain"(...) functions';
2307 COMMENT ON COLUMN "delegation_chain_row"."index" IS 'Index starting with 0 and counting up';
2308 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';
2309 COMMENT ON COLUMN "delegation_chain_row"."overridden" IS 'True, if an entry with lower index has "participation" set to true';
2310 COMMENT ON COLUMN "delegation_chain_row"."scope_in" IS 'Scope of used incoming delegation';
2311 COMMENT ON COLUMN "delegation_chain_row"."scope_out" IS 'Scope of used outgoing delegation';
2312 COMMENT ON COLUMN "delegation_chain_row"."disabled_out" IS 'Outgoing delegation is explicitly disabled by a delegation with trustee_id set to NULL';
2313 COMMENT ON COLUMN "delegation_chain_row"."loop" IS 'Not null, if member is part of a loop, see "delegation_chain_loop_tag" type';
2316 CREATE FUNCTION "delegation_chain"
2317 ( "member_id_p" "member"."id"%TYPE,
2318 "unit_id_p" "unit"."id"%TYPE,
2319 "area_id_p" "area"."id"%TYPE,
2320 "issue_id_p" "issue"."id"%TYPE,
2321 "simulate_trustee_id_p" "member"."id"%TYPE )
2322 RETURNS SETOF "delegation_chain_row"
2323 LANGUAGE 'plpgsql' STABLE AS $$
2324 DECLARE
2325 "scope_v" "delegation_scope";
2326 "unit_id_v" "unit"."id"%TYPE;
2327 "area_id_v" "area"."id"%TYPE;
2328 "visited_member_ids" INT4[]; -- "member"."id"%TYPE[]
2329 "loop_member_id_v" "member"."id"%TYPE;
2330 "output_row" "delegation_chain_row";
2331 "output_rows" "delegation_chain_row"[];
2332 "delegation_row" "delegation"%ROWTYPE;
2333 "row_count" INT4;
2334 "i" INT4;
2335 "loop_v" BOOLEAN;
2336 BEGIN
2337 IF
2338 "unit_id_p" NOTNULL AND
2339 "area_id_p" ISNULL AND
2340 "issue_id_p" ISNULL
2341 THEN
2342 "scope_v" := 'unit';
2343 "unit_id_v" := "unit_id_p";
2344 ELSIF
2345 "unit_id_p" ISNULL AND
2346 "area_id_p" NOTNULL AND
2347 "issue_id_p" ISNULL
2348 THEN
2349 "scope_v" := 'area';
2350 "area_id_v" := "area_id_p";
2351 SELECT "unit_id" INTO "unit_id_v"
2352 FROM "area" WHERE "id" = "area_id_v";
2353 ELSIF
2354 "unit_id_p" ISNULL AND
2355 "area_id_p" ISNULL AND
2356 "issue_id_p" NOTNULL
2357 THEN
2358 "scope_v" := 'issue';
2359 SELECT "area_id" INTO "area_id_v"
2360 FROM "issue" WHERE "id" = "issue_id_p";
2361 SELECT "unit_id" INTO "unit_id_v"
2362 FROM "area" WHERE "id" = "area_id_v";
2363 ELSE
2364 RAISE EXCEPTION 'Exactly one of unit_id_p, area_id_p, or issue_id_p must be NOTNULL.';
2365 END IF;
2366 "visited_member_ids" := '{}';
2367 "loop_member_id_v" := NULL;
2368 "output_rows" := '{}';
2369 "output_row"."index" := 0;
2370 "output_row"."member_id" := "member_id_p";
2371 "output_row"."member_valid" := TRUE;
2372 "output_row"."participation" := FALSE;
2373 "output_row"."overridden" := FALSE;
2374 "output_row"."disabled_out" := FALSE;
2375 "output_row"."scope_out" := NULL;
2376 LOOP
2377 IF "visited_member_ids" @> ARRAY["output_row"."member_id"] THEN
2378 "loop_member_id_v" := "output_row"."member_id";
2379 ELSE
2380 "visited_member_ids" :=
2381 "visited_member_ids" || "output_row"."member_id";
2382 END IF;
2383 IF "output_row"."participation" THEN
2384 "output_row"."overridden" := TRUE;
2385 END IF;
2386 "output_row"."scope_in" := "output_row"."scope_out";
2387 IF EXISTS (
2388 SELECT NULL FROM "member" JOIN "privilege"
2389 ON "privilege"."member_id" = "member"."id"
2390 AND "privilege"."unit_id" = "unit_id_v"
2391 WHERE "id" = "output_row"."member_id"
2392 AND "member"."active" AND "privilege"."voting_right"
2393 ) THEN
2394 IF "scope_v" = 'unit' THEN
2395 SELECT * INTO "delegation_row" FROM "delegation"
2396 WHERE "truster_id" = "output_row"."member_id"
2397 AND "unit_id" = "unit_id_v";
2398 ELSIF "scope_v" = 'area' THEN
2399 "output_row"."participation" := EXISTS (
2400 SELECT NULL FROM "membership"
2401 WHERE "area_id" = "area_id_p"
2402 AND "member_id" = "output_row"."member_id"
2403 );
2404 SELECT * INTO "delegation_row" FROM "delegation"
2405 WHERE "truster_id" = "output_row"."member_id"
2406 AND (
2407 "unit_id" = "unit_id_v" OR
2408 "area_id" = "area_id_v"
2409 )
2410 ORDER BY "scope" DESC;
2411 ELSIF "scope_v" = 'issue' THEN
2412 "output_row"."participation" := EXISTS (
2413 SELECT NULL FROM "interest"
2414 WHERE "issue_id" = "issue_id_p"
2415 AND "member_id" = "output_row"."member_id"
2416 );
2417 SELECT * INTO "delegation_row" FROM "delegation"
2418 WHERE "truster_id" = "output_row"."member_id"
2419 AND (
2420 "unit_id" = "unit_id_v" OR
2421 "area_id" = "area_id_v" OR
2422 "issue_id" = "issue_id_p"
2423 )
2424 ORDER BY "scope" DESC;
2425 END IF;
2426 ELSE
2427 "output_row"."member_valid" := FALSE;
2428 "output_row"."participation" := FALSE;
2429 "output_row"."scope_out" := NULL;
2430 "delegation_row" := ROW(NULL);
2431 END IF;
2432 IF
2433 "output_row"."member_id" = "member_id_p" AND
2434 "simulate_trustee_id_p" NOTNULL
2435 THEN
2436 "output_row"."scope_out" := "scope_v";
2437 "output_rows" := "output_rows" || "output_row";
2438 "output_row"."member_id" := "simulate_trustee_id_p";
2439 ELSIF "delegation_row"."trustee_id" NOTNULL THEN
2440 "output_row"."scope_out" := "delegation_row"."scope";
2441 "output_rows" := "output_rows" || "output_row";
2442 "output_row"."member_id" := "delegation_row"."trustee_id";
2443 ELSIF "delegation_row"."scope" NOTNULL THEN
2444 "output_row"."scope_out" := "delegation_row"."scope";
2445 "output_row"."disabled_out" := TRUE;
2446 "output_rows" := "output_rows" || "output_row";
2447 EXIT;
2448 ELSE
2449 "output_row"."scope_out" := NULL;
2450 "output_rows" := "output_rows" || "output_row";
2451 EXIT;
2452 END IF;
2453 EXIT WHEN "loop_member_id_v" NOTNULL;
2454 "output_row"."index" := "output_row"."index" + 1;
2455 END LOOP;
2456 "row_count" := array_upper("output_rows", 1);
2457 "i" := 1;
2458 "loop_v" := FALSE;
2459 LOOP
2460 "output_row" := "output_rows"["i"];
2461 EXIT WHEN "output_row" ISNULL; -- NOTE: ISNULL and NOT ... NOTNULL produce different results!
2462 IF "loop_v" THEN
2463 IF "i" + 1 = "row_count" THEN
2464 "output_row"."loop" := 'last';
2465 ELSIF "i" = "row_count" THEN
2466 "output_row"."loop" := 'repetition';
2467 ELSE
2468 "output_row"."loop" := 'intermediate';
2469 END IF;
2470 ELSIF "output_row"."member_id" = "loop_member_id_v" THEN
2471 "output_row"."loop" := 'first';
2472 "loop_v" := TRUE;
2473 END IF;
2474 IF "scope_v" = 'unit' THEN
2475 "output_row"."participation" := NULL;
2476 END IF;
2477 RETURN NEXT "output_row";
2478 "i" := "i" + 1;
2479 END LOOP;
2480 RETURN;
2481 END;
2482 $$;
2484 COMMENT ON FUNCTION "delegation_chain"
2485 ( "member"."id"%TYPE,
2486 "unit"."id"%TYPE,
2487 "area"."id"%TYPE,
2488 "issue"."id"%TYPE,
2489 "member"."id"%TYPE )
2490 IS 'Helper function for frontends to display delegation chains; Not part of internal voting logic';
2493 CREATE FUNCTION "delegation_chain"
2494 ( "member_id_p" "member"."id"%TYPE,
2495 "unit_id_p" "unit"."id"%TYPE,
2496 "area_id_p" "area"."id"%TYPE,
2497 "issue_id_p" "issue"."id"%TYPE )
2498 RETURNS SETOF "delegation_chain_row"
2499 LANGUAGE 'plpgsql' STABLE AS $$
2500 DECLARE
2501 "result_row" "delegation_chain_row";
2502 BEGIN
2503 FOR "result_row" IN
2504 SELECT * FROM "delegation_chain"(
2505 "member_id_p", "unit_id_p", "area_id_p", "issue_id_p", NULL
2506 )
2507 LOOP
2508 RETURN NEXT "result_row";
2509 END LOOP;
2510 RETURN;
2511 END;
2512 $$;
2514 COMMENT ON FUNCTION "delegation_chain"
2515 ( "member"."id"%TYPE,
2516 "unit"."id"%TYPE,
2517 "area"."id"%TYPE,
2518 "issue"."id"%TYPE )
2519 IS 'Shortcut for "delegation_chain"(...) function where 4th parameter is null';
2523 ------------------------------
2524 -- Comparison by vote count --
2525 ------------------------------
2527 CREATE FUNCTION "vote_ratio"
2528 ( "positive_votes_p" "initiative"."positive_votes"%TYPE,
2529 "negative_votes_p" "initiative"."negative_votes"%TYPE )
2530 RETURNS FLOAT8
2531 LANGUAGE 'plpgsql' STABLE AS $$
2532 BEGIN
2533 IF "positive_votes_p" > 0 AND "negative_votes_p" > 0 THEN
2534 RETURN
2535 "positive_votes_p"::FLOAT8 /
2536 ("positive_votes_p" + "negative_votes_p")::FLOAT8;
2537 ELSIF "positive_votes_p" > 0 THEN
2538 RETURN "positive_votes_p";
2539 ELSIF "negative_votes_p" > 0 THEN
2540 RETURN 1 - "negative_votes_p";
2541 ELSE
2542 RETURN 0.5;
2543 END IF;
2544 END;
2545 $$;
2547 COMMENT ON FUNCTION "vote_ratio"
2548 ( "initiative"."positive_votes"%TYPE,
2549 "initiative"."negative_votes"%TYPE )
2550 IS 'Returns a number, which can be used for comparison of initiatives based on count of approvals and disapprovals. Greater numbers indicate a better result. This function is NOT injective.';
2554 ------------------------------------------------
2555 -- Locking for snapshots and voting procedure --
2556 ------------------------------------------------
2559 CREATE FUNCTION "share_row_lock_issue_trigger"()
2560 RETURNS TRIGGER
2561 LANGUAGE 'plpgsql' VOLATILE AS $$
2562 BEGIN
2563 IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
2564 PERFORM NULL FROM "issue" WHERE "id" = OLD."issue_id" FOR SHARE;
2565 END IF;
2566 IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
2567 PERFORM NULL FROM "issue" WHERE "id" = NEW."issue_id" FOR SHARE;
2568 RETURN NEW;
2569 ELSE
2570 RETURN OLD;
2571 END IF;
2572 END;
2573 $$;
2575 COMMENT ON FUNCTION "share_row_lock_issue_trigger"() IS 'Implementation of triggers "share_row_lock_issue" on multiple tables';
2578 CREATE FUNCTION "share_row_lock_issue_via_initiative_trigger"()
2579 RETURNS TRIGGER
2580 LANGUAGE 'plpgsql' VOLATILE AS $$
2581 BEGIN
2582 IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
2583 PERFORM NULL FROM "issue"
2584 JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
2585 WHERE "initiative"."id" = OLD."initiative_id"
2586 FOR SHARE OF "issue";
2587 END IF;
2588 IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
2589 PERFORM NULL FROM "issue"
2590 JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
2591 WHERE "initiative"."id" = NEW."initiative_id"
2592 FOR SHARE OF "issue";
2593 RETURN NEW;
2594 ELSE
2595 RETURN OLD;
2596 END IF;
2597 END;
2598 $$;
2600 COMMENT ON FUNCTION "share_row_lock_issue_trigger"() IS 'Implementation of trigger "share_row_lock_issue_via_initiative" on table "opinion"';
2603 CREATE TRIGGER "share_row_lock_issue"
2604 BEFORE INSERT OR UPDATE OR DELETE ON "initiative"
2605 FOR EACH ROW EXECUTE PROCEDURE
2606 "share_row_lock_issue_trigger"();
2608 CREATE TRIGGER "share_row_lock_issue"
2609 BEFORE INSERT OR UPDATE OR DELETE ON "interest"
2610 FOR EACH ROW EXECUTE PROCEDURE
2611 "share_row_lock_issue_trigger"();
2613 CREATE TRIGGER "share_row_lock_issue"
2614 BEFORE INSERT OR UPDATE OR DELETE ON "supporter"
2615 FOR EACH ROW EXECUTE PROCEDURE
2616 "share_row_lock_issue_trigger"();
2618 CREATE TRIGGER "share_row_lock_issue_via_initiative"
2619 BEFORE INSERT OR UPDATE OR DELETE ON "opinion"
2620 FOR EACH ROW EXECUTE PROCEDURE
2621 "share_row_lock_issue_via_initiative_trigger"();
2623 CREATE TRIGGER "share_row_lock_issue"
2624 BEFORE INSERT OR UPDATE OR DELETE ON "direct_voter"
2625 FOR EACH ROW EXECUTE PROCEDURE
2626 "share_row_lock_issue_trigger"();
2628 CREATE TRIGGER "share_row_lock_issue"
2629 BEFORE INSERT OR UPDATE OR DELETE ON "delegating_voter"
2630 FOR EACH ROW EXECUTE PROCEDURE
2631 "share_row_lock_issue_trigger"();
2633 CREATE TRIGGER "share_row_lock_issue"
2634 BEFORE INSERT OR UPDATE OR DELETE ON "vote"
2635 FOR EACH ROW EXECUTE PROCEDURE
2636 "share_row_lock_issue_trigger"();
2638 COMMENT ON TRIGGER "share_row_lock_issue" ON "initiative" IS 'See "lock_issue" function';
2639 COMMENT ON TRIGGER "share_row_lock_issue" ON "interest" IS 'See "lock_issue" function';
2640 COMMENT ON TRIGGER "share_row_lock_issue" ON "supporter" IS 'See "lock_issue" function';
2641 COMMENT ON TRIGGER "share_row_lock_issue_via_initiative" ON "opinion" IS 'See "lock_issue" function';
2642 COMMENT ON TRIGGER "share_row_lock_issue" ON "direct_voter" IS 'See "lock_issue" function';
2643 COMMENT ON TRIGGER "share_row_lock_issue" ON "delegating_voter" IS 'See "lock_issue" function';
2644 COMMENT ON TRIGGER "share_row_lock_issue" ON "vote" IS 'See "lock_issue" function';
2647 CREATE FUNCTION "lock_issue"
2648 ( "issue_id_p" "issue"."id"%TYPE )
2649 RETURNS VOID
2650 LANGUAGE 'plpgsql' VOLATILE AS $$
2651 BEGIN
2652 LOCK TABLE "member" IN SHARE MODE;
2653 LOCK TABLE "privilege" IN SHARE MODE;
2654 LOCK TABLE "membership" IN SHARE MODE;
2655 LOCK TABLE "policy" IN SHARE MODE;
2656 PERFORM NULL FROM "issue" WHERE "id" = "issue_id_p" FOR UPDATE;
2657 -- NOTE: The row-level exclusive lock in combination with the
2658 -- share_row_lock_issue(_via_initiative)_trigger functions (which
2659 -- acquire a row-level share lock on the issue) ensure that no data
2660 -- is changed, which could affect calculation of snapshots or
2661 -- counting of votes. Table "delegation" must be table-level-locked,
2662 -- as it also contains issue- and global-scope delegations.
2663 LOCK TABLE "delegation" IN SHARE MODE;
2664 LOCK TABLE "direct_population_snapshot" IN EXCLUSIVE MODE;
2665 LOCK TABLE "delegating_population_snapshot" IN EXCLUSIVE MODE;
2666 LOCK TABLE "direct_interest_snapshot" IN EXCLUSIVE MODE;
2667 LOCK TABLE "delegating_interest_snapshot" IN EXCLUSIVE MODE;
2668 LOCK TABLE "direct_supporter_snapshot" IN EXCLUSIVE MODE;
2669 RETURN;
2670 END;
2671 $$;
2673 COMMENT ON FUNCTION "lock_issue"
2674 ( "issue"."id"%TYPE )
2675 IS 'Locks the issue and all other data which is used for calculating snapshots or counting votes.';
2679 ------------------------------------------------------------------------
2680 -- Regular tasks, except calculcation of snapshots and voting results --
2681 ------------------------------------------------------------------------
2683 CREATE FUNCTION "check_last_login"()
2684 RETURNS VOID
2685 LANGUAGE 'plpgsql' VOLATILE AS $$
2686 DECLARE
2687 "system_setting_row" "system_setting"%ROWTYPE;
2688 BEGIN
2689 SELECT * INTO "system_setting_row" FROM "system_setting";
2690 LOCK TABLE "member" IN SHARE ROW EXCLUSIVE MODE;
2691 UPDATE "member" SET "last_login_public" = "last_login"::date
2692 FROM (
2693 SELECT DISTINCT "member"."id"
2694 FROM "member" LEFT JOIN "member_history"
2695 ON "member"."id" = "member_history"."member_id"
2696 WHERE "member"."last_login"::date < 'today' OR (
2697 "member_history"."until"::date >= 'today' AND
2698 "member_history"."active" = FALSE AND "member"."active" = TRUE
2699 )
2700 ) AS "subquery"
2701 WHERE "member"."id" = "subquery"."id";
2702 IF "system_setting_row"."member_ttl" NOTNULL THEN
2703 UPDATE "member" SET "active" = FALSE
2704 WHERE "active" = TRUE
2705 AND "last_login"::date < 'today'
2706 AND "last_login_public" <
2707 (now() - "system_setting_row"."member_ttl")::date;
2708 END IF;
2709 RETURN;
2710 END;
2711 $$;
2713 COMMENT ON FUNCTION "check_last_login"() IS 'Updates "last_login_public" field, which contains the date but not the time of the last login, and deactivates members who do not login for the time specified in "system_setting"."member_ttl". For privacy reasons this function does not update "last_login_public", if the last login of a member has been today (except when member was reactivated today).';
2716 CREATE FUNCTION "calculate_member_counts"()
2717 RETURNS VOID
2718 LANGUAGE 'plpgsql' VOLATILE AS $$
2719 BEGIN
2720 LOCK TABLE "member" IN SHARE MODE;
2721 LOCK TABLE "member_count" IN EXCLUSIVE MODE;
2722 LOCK TABLE "unit" IN EXCLUSIVE MODE;
2723 LOCK TABLE "area" IN EXCLUSIVE MODE;
2724 LOCK TABLE "privilege" IN SHARE MODE;
2725 LOCK TABLE "membership" IN SHARE MODE;
2726 DELETE FROM "member_count";
2727 INSERT INTO "member_count" ("total_count")
2728 SELECT "total_count" FROM "member_count_view";
2729 UPDATE "unit" SET "member_count" = "view"."member_count"
2730 FROM "unit_member_count" AS "view"
2731 WHERE "view"."unit_id" = "unit"."id";
2732 UPDATE "area" SET
2733 "direct_member_count" = "view"."direct_member_count",
2734 "member_weight" = "view"."member_weight"
2735 FROM "area_member_count" AS "view"
2736 WHERE "view"."area_id" = "area"."id";
2737 RETURN;
2738 END;
2739 $$;
2741 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"';
2745 ------------------------------
2746 -- Calculation of snapshots --
2747 ------------------------------
2749 CREATE FUNCTION "weight_of_added_delegations_for_population_snapshot"
2750 ( "issue_id_p" "issue"."id"%TYPE,
2751 "member_id_p" "member"."id"%TYPE,
2752 "delegate_member_ids_p" "delegating_population_snapshot"."delegate_member_ids"%TYPE )
2753 RETURNS "direct_population_snapshot"."weight"%TYPE
2754 LANGUAGE 'plpgsql' VOLATILE AS $$
2755 DECLARE
2756 "issue_delegation_row" "issue_delegation"%ROWTYPE;
2757 "delegate_member_ids_v" "delegating_population_snapshot"."delegate_member_ids"%TYPE;
2758 "weight_v" INT4;
2759 "sub_weight_v" INT4;
2760 BEGIN
2761 "weight_v" := 0;
2762 FOR "issue_delegation_row" IN
2763 SELECT * FROM "issue_delegation"
2764 WHERE "trustee_id" = "member_id_p"
2765 AND "issue_id" = "issue_id_p"
2766 LOOP
2767 IF NOT EXISTS (
2768 SELECT NULL FROM "direct_population_snapshot"
2769 WHERE "issue_id" = "issue_id_p"
2770 AND "event" = 'periodic'
2771 AND "member_id" = "issue_delegation_row"."truster_id"
2772 ) AND NOT EXISTS (
2773 SELECT NULL FROM "delegating_population_snapshot"
2774 WHERE "issue_id" = "issue_id_p"
2775 AND "event" = 'periodic'
2776 AND "member_id" = "issue_delegation_row"."truster_id"
2777 ) THEN
2778 "delegate_member_ids_v" :=
2779 "member_id_p" || "delegate_member_ids_p";
2780 INSERT INTO "delegating_population_snapshot" (
2781 "issue_id",
2782 "event",
2783 "member_id",
2784 "scope",
2785 "delegate_member_ids"
2786 ) VALUES (
2787 "issue_id_p",
2788 'periodic',
2789 "issue_delegation_row"."truster_id",
2790 "issue_delegation_row"."scope",
2791 "delegate_member_ids_v"
2792 );
2793 "sub_weight_v" := 1 +
2794 "weight_of_added_delegations_for_population_snapshot"(
2795 "issue_id_p",
2796 "issue_delegation_row"."truster_id",
2797 "delegate_member_ids_v"
2798 );
2799 UPDATE "delegating_population_snapshot"
2800 SET "weight" = "sub_weight_v"
2801 WHERE "issue_id" = "issue_id_p"
2802 AND "event" = 'periodic'
2803 AND "member_id" = "issue_delegation_row"."truster_id";
2804 "weight_v" := "weight_v" + "sub_weight_v";
2805 END IF;
2806 END LOOP;
2807 RETURN "weight_v";
2808 END;
2809 $$;
2811 COMMENT ON FUNCTION "weight_of_added_delegations_for_population_snapshot"
2812 ( "issue"."id"%TYPE,
2813 "member"."id"%TYPE,
2814 "delegating_population_snapshot"."delegate_member_ids"%TYPE )
2815 IS 'Helper function for "create_population_snapshot" function';
2818 CREATE FUNCTION "create_population_snapshot"
2819 ( "issue_id_p" "issue"."id"%TYPE )
2820 RETURNS VOID
2821 LANGUAGE 'plpgsql' VOLATILE AS $$
2822 DECLARE
2823 "member_id_v" "member"."id"%TYPE;
2824 BEGIN
2825 DELETE FROM "direct_population_snapshot"
2826 WHERE "issue_id" = "issue_id_p"
2827 AND "event" = 'periodic';
2828 DELETE FROM "delegating_population_snapshot"
2829 WHERE "issue_id" = "issue_id_p"
2830 AND "event" = 'periodic';
2831 INSERT INTO "direct_population_snapshot"
2832 ("issue_id", "event", "member_id")
2833 SELECT
2834 "issue_id_p" AS "issue_id",
2835 'periodic'::"snapshot_event" AS "event",
2836 "member"."id" AS "member_id"
2837 FROM "issue"
2838 JOIN "area" ON "issue"."area_id" = "area"."id"
2839 JOIN "membership" ON "area"."id" = "membership"."area_id"
2840 JOIN "member" ON "membership"."member_id" = "member"."id"
2841 JOIN "privilege"
2842 ON "privilege"."unit_id" = "area"."unit_id"
2843 AND "privilege"."member_id" = "member"."id"
2844 WHERE "issue"."id" = "issue_id_p"
2845 AND "member"."active" AND "privilege"."voting_right"
2846 UNION
2847 SELECT
2848 "issue_id_p" AS "issue_id",
2849 'periodic'::"snapshot_event" AS "event",
2850 "member"."id" AS "member_id"
2851 FROM "issue"
2852 JOIN "area" ON "issue"."area_id" = "area"."id"
2853 JOIN "interest" ON "issue"."id" = "interest"."issue_id"
2854 JOIN "member" ON "interest"."member_id" = "member"."id"
2855 JOIN "privilege"
2856 ON "privilege"."unit_id" = "area"."unit_id"
2857 AND "privilege"."member_id" = "member"."id"
2858 WHERE "issue"."id" = "issue_id_p"
2859 AND "member"."active" AND "privilege"."voting_right";
2860 FOR "member_id_v" IN
2861 SELECT "member_id" FROM "direct_population_snapshot"
2862 WHERE "issue_id" = "issue_id_p"
2863 AND "event" = 'periodic'
2864 LOOP
2865 UPDATE "direct_population_snapshot" SET
2866 "weight" = 1 +
2867 "weight_of_added_delegations_for_population_snapshot"(
2868 "issue_id_p",
2869 "member_id_v",
2870 '{}'
2871 )
2872 WHERE "issue_id" = "issue_id_p"
2873 AND "event" = 'periodic'
2874 AND "member_id" = "member_id_v";
2875 END LOOP;
2876 RETURN;
2877 END;
2878 $$;
2880 COMMENT ON FUNCTION "create_population_snapshot"
2881 ( "issue"."id"%TYPE )
2882 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.';
2885 CREATE FUNCTION "weight_of_added_delegations_for_interest_snapshot"
2886 ( "issue_id_p" "issue"."id"%TYPE,
2887 "member_id_p" "member"."id"%TYPE,
2888 "delegate_member_ids_p" "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
2889 RETURNS "direct_interest_snapshot"."weight"%TYPE
2890 LANGUAGE 'plpgsql' VOLATILE AS $$
2891 DECLARE
2892 "issue_delegation_row" "issue_delegation"%ROWTYPE;
2893 "delegate_member_ids_v" "delegating_interest_snapshot"."delegate_member_ids"%TYPE;
2894 "weight_v" INT4;
2895 "sub_weight_v" INT4;
2896 BEGIN
2897 "weight_v" := 0;
2898 FOR "issue_delegation_row" IN
2899 SELECT * FROM "issue_delegation"
2900 WHERE "trustee_id" = "member_id_p"
2901 AND "issue_id" = "issue_id_p"
2902 LOOP
2903 IF NOT EXISTS (
2904 SELECT NULL FROM "direct_interest_snapshot"
2905 WHERE "issue_id" = "issue_id_p"
2906 AND "event" = 'periodic'
2907 AND "member_id" = "issue_delegation_row"."truster_id"
2908 ) AND NOT EXISTS (
2909 SELECT NULL FROM "delegating_interest_snapshot"
2910 WHERE "issue_id" = "issue_id_p"
2911 AND "event" = 'periodic'
2912 AND "member_id" = "issue_delegation_row"."truster_id"
2913 ) THEN
2914 "delegate_member_ids_v" :=
2915 "member_id_p" || "delegate_member_ids_p";
2916 INSERT INTO "delegating_interest_snapshot" (
2917 "issue_id",
2918 "event",
2919 "member_id",
2920 "scope",
2921 "delegate_member_ids"
2922 ) VALUES (
2923 "issue_id_p",
2924 'periodic',
2925 "issue_delegation_row"."truster_id",
2926 "issue_delegation_row"."scope",
2927 "delegate_member_ids_v"
2928 );
2929 "sub_weight_v" := 1 +
2930 "weight_of_added_delegations_for_interest_snapshot"(
2931 "issue_id_p",
2932 "issue_delegation_row"."truster_id",
2933 "delegate_member_ids_v"
2934 );
2935 UPDATE "delegating_interest_snapshot"
2936 SET "weight" = "sub_weight_v"
2937 WHERE "issue_id" = "issue_id_p"
2938 AND "event" = 'periodic'
2939 AND "member_id" = "issue_delegation_row"."truster_id";
2940 "weight_v" := "weight_v" + "sub_weight_v";
2941 END IF;
2942 END LOOP;
2943 RETURN "weight_v";
2944 END;
2945 $$;
2947 COMMENT ON FUNCTION "weight_of_added_delegations_for_interest_snapshot"
2948 ( "issue"."id"%TYPE,
2949 "member"."id"%TYPE,
2950 "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
2951 IS 'Helper function for "create_interest_snapshot" function';
2954 CREATE FUNCTION "create_interest_snapshot"
2955 ( "issue_id_p" "issue"."id"%TYPE )
2956 RETURNS VOID
2957 LANGUAGE 'plpgsql' VOLATILE AS $$
2958 DECLARE
2959 "member_id_v" "member"."id"%TYPE;
2960 BEGIN
2961 DELETE FROM "direct_interest_snapshot"
2962 WHERE "issue_id" = "issue_id_p"
2963 AND "event" = 'periodic';
2964 DELETE FROM "delegating_interest_snapshot"
2965 WHERE "issue_id" = "issue_id_p"
2966 AND "event" = 'periodic';
2967 DELETE FROM "direct_supporter_snapshot"
2968 WHERE "issue_id" = "issue_id_p"
2969 AND "event" = 'periodic';
2970 INSERT INTO "direct_interest_snapshot"
2971 ("issue_id", "event", "member_id")
2972 SELECT
2973 "issue_id_p" AS "issue_id",
2974 'periodic' AS "event",
2975 "member"."id" AS "member_id"
2976 FROM "issue"
2977 JOIN "area" ON "issue"."area_id" = "area"."id"
2978 JOIN "interest" ON "issue"."id" = "interest"."issue_id"
2979 JOIN "member" ON "interest"."member_id" = "member"."id"
2980 JOIN "privilege"
2981 ON "privilege"."unit_id" = "area"."unit_id"
2982 AND "privilege"."member_id" = "member"."id"
2983 WHERE "issue"."id" = "issue_id_p"
2984 AND "member"."active" AND "privilege"."voting_right";
2985 FOR "member_id_v" IN
2986 SELECT "member_id" FROM "direct_interest_snapshot"
2987 WHERE "issue_id" = "issue_id_p"
2988 AND "event" = 'periodic'
2989 LOOP
2990 UPDATE "direct_interest_snapshot" SET
2991 "weight" = 1 +
2992 "weight_of_added_delegations_for_interest_snapshot"(
2993 "issue_id_p",
2994 "member_id_v",
2995 '{}'
2996 )
2997 WHERE "issue_id" = "issue_id_p"
2998 AND "event" = 'periodic'
2999 AND "member_id" = "member_id_v";
3000 END LOOP;
3001 INSERT INTO "direct_supporter_snapshot"
3002 ( "issue_id", "initiative_id", "event", "member_id",
3003 "informed", "satisfied" )
3004 SELECT
3005 "issue_id_p" AS "issue_id",
3006 "initiative"."id" AS "initiative_id",
3007 'periodic' AS "event",
3008 "supporter"."member_id" AS "member_id",
3009 "supporter"."draft_id" = "current_draft"."id" AS "informed",
3010 NOT EXISTS (
3011 SELECT NULL FROM "critical_opinion"
3012 WHERE "initiative_id" = "initiative"."id"
3013 AND "member_id" = "supporter"."member_id"
3014 ) AS "satisfied"
3015 FROM "initiative"
3016 JOIN "supporter"
3017 ON "supporter"."initiative_id" = "initiative"."id"
3018 JOIN "current_draft"
3019 ON "initiative"."id" = "current_draft"."initiative_id"
3020 JOIN "direct_interest_snapshot"
3021 ON "supporter"."member_id" = "direct_interest_snapshot"."member_id"
3022 AND "initiative"."issue_id" = "direct_interest_snapshot"."issue_id"
3023 AND "event" = 'periodic'
3024 WHERE "initiative"."issue_id" = "issue_id_p";
3025 RETURN;
3026 END;
3027 $$;
3029 COMMENT ON FUNCTION "create_interest_snapshot"
3030 ( "issue"."id"%TYPE )
3031 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.';
3034 CREATE FUNCTION "create_snapshot"
3035 ( "issue_id_p" "issue"."id"%TYPE )
3036 RETURNS VOID
3037 LANGUAGE 'plpgsql' VOLATILE AS $$
3038 DECLARE
3039 "initiative_id_v" "initiative"."id"%TYPE;
3040 "suggestion_id_v" "suggestion"."id"%TYPE;
3041 BEGIN
3042 PERFORM "lock_issue"("issue_id_p");
3043 PERFORM "create_population_snapshot"("issue_id_p");
3044 PERFORM "create_interest_snapshot"("issue_id_p");
3045 UPDATE "issue" SET
3046 "snapshot" = now(),
3047 "latest_snapshot_event" = 'periodic',
3048 "population" = (
3049 SELECT coalesce(sum("weight"), 0)
3050 FROM "direct_population_snapshot"
3051 WHERE "issue_id" = "issue_id_p"
3052 AND "event" = 'periodic'
3053 )
3054 WHERE "id" = "issue_id_p";
3055 FOR "initiative_id_v" IN
3056 SELECT "id" FROM "initiative" WHERE "issue_id" = "issue_id_p"
3057 LOOP
3058 UPDATE "initiative" SET
3059 "supporter_count" = (
3060 SELECT coalesce(sum("di"."weight"), 0)
3061 FROM "direct_interest_snapshot" AS "di"
3062 JOIN "direct_supporter_snapshot" AS "ds"
3063 ON "di"."member_id" = "ds"."member_id"
3064 WHERE "di"."issue_id" = "issue_id_p"
3065 AND "di"."event" = 'periodic'
3066 AND "ds"."initiative_id" = "initiative_id_v"
3067 AND "ds"."event" = 'periodic'
3068 ),
3069 "informed_supporter_count" = (
3070 SELECT coalesce(sum("di"."weight"), 0)
3071 FROM "direct_interest_snapshot" AS "di"
3072 JOIN "direct_supporter_snapshot" AS "ds"
3073 ON "di"."member_id" = "ds"."member_id"
3074 WHERE "di"."issue_id" = "issue_id_p"
3075 AND "di"."event" = 'periodic'
3076 AND "ds"."initiative_id" = "initiative_id_v"
3077 AND "ds"."event" = 'periodic'
3078 AND "ds"."informed"
3079 ),
3080 "satisfied_supporter_count" = (
3081 SELECT coalesce(sum("di"."weight"), 0)
3082 FROM "direct_interest_snapshot" AS "di"
3083 JOIN "direct_supporter_snapshot" AS "ds"
3084 ON "di"."member_id" = "ds"."member_id"
3085 WHERE "di"."issue_id" = "issue_id_p"
3086 AND "di"."event" = 'periodic'
3087 AND "ds"."initiative_id" = "initiative_id_v"
3088 AND "ds"."event" = 'periodic'
3089 AND "ds"."satisfied"
3090 ),
3091 "satisfied_informed_supporter_count" = (
3092 SELECT coalesce(sum("di"."weight"), 0)
3093 FROM "direct_interest_snapshot" AS "di"
3094 JOIN "direct_supporter_snapshot" AS "ds"
3095 ON "di"."member_id" = "ds"."member_id"
3096 WHERE "di"."issue_id" = "issue_id_p"
3097 AND "di"."event" = 'periodic'
3098 AND "ds"."initiative_id" = "initiative_id_v"
3099 AND "ds"."event" = 'periodic'
3100 AND "ds"."informed"
3101 AND "ds"."satisfied"
3102 )
3103 WHERE "id" = "initiative_id_v";
3104 FOR "suggestion_id_v" IN
3105 SELECT "id" FROM "suggestion"
3106 WHERE "initiative_id" = "initiative_id_v"
3107 LOOP
3108 UPDATE "suggestion" SET
3109 "minus2_unfulfilled_count" = (
3110 SELECT coalesce(sum("snapshot"."weight"), 0)
3111 FROM "issue" CROSS JOIN "opinion"
3112 JOIN "direct_interest_snapshot" AS "snapshot"
3113 ON "snapshot"."issue_id" = "issue"."id"
3114 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3115 AND "snapshot"."member_id" = "opinion"."member_id"
3116 WHERE "issue"."id" = "issue_id_p"
3117 AND "opinion"."suggestion_id" = "suggestion_id_v"
3118 AND "opinion"."degree" = -2
3119 AND "opinion"."fulfilled" = FALSE
3120 ),
3121 "minus2_fulfilled_count" = (
3122 SELECT coalesce(sum("snapshot"."weight"), 0)
3123 FROM "issue" CROSS JOIN "opinion"
3124 JOIN "direct_interest_snapshot" AS "snapshot"
3125 ON "snapshot"."issue_id" = "issue"."id"
3126 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3127 AND "snapshot"."member_id" = "opinion"."member_id"
3128 WHERE "issue"."id" = "issue_id_p"
3129 AND "opinion"."suggestion_id" = "suggestion_id_v"
3130 AND "opinion"."degree" = -2
3131 AND "opinion"."fulfilled" = TRUE
3132 ),
3133 "minus1_unfulfilled_count" = (
3134 SELECT coalesce(sum("snapshot"."weight"), 0)
3135 FROM "issue" CROSS JOIN "opinion"
3136 JOIN "direct_interest_snapshot" AS "snapshot"
3137 ON "snapshot"."issue_id" = "issue"."id"
3138 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3139 AND "snapshot"."member_id" = "opinion"."member_id"
3140 WHERE "issue"."id" = "issue_id_p"
3141 AND "opinion"."suggestion_id" = "suggestion_id_v"
3142 AND "opinion"."degree" = -1
3143 AND "opinion"."fulfilled" = FALSE
3144 ),
3145 "minus1_fulfilled_count" = (
3146 SELECT coalesce(sum("snapshot"."weight"), 0)
3147 FROM "issue" CROSS JOIN "opinion"
3148 JOIN "direct_interest_snapshot" AS "snapshot"
3149 ON "snapshot"."issue_id" = "issue"."id"
3150 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3151 AND "snapshot"."member_id" = "opinion"."member_id"
3152 WHERE "issue"."id" = "issue_id_p"
3153 AND "opinion"."suggestion_id" = "suggestion_id_v"
3154 AND "opinion"."degree" = -1
3155 AND "opinion"."fulfilled" = TRUE
3156 ),
3157 "plus1_unfulfilled_count" = (
3158 SELECT coalesce(sum("snapshot"."weight"), 0)
3159 FROM "issue" CROSS JOIN "opinion"
3160 JOIN "direct_interest_snapshot" AS "snapshot"
3161 ON "snapshot"."issue_id" = "issue"."id"
3162 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3163 AND "snapshot"."member_id" = "opinion"."member_id"
3164 WHERE "issue"."id" = "issue_id_p"
3165 AND "opinion"."suggestion_id" = "suggestion_id_v"
3166 AND "opinion"."degree" = 1
3167 AND "opinion"."fulfilled" = FALSE
3168 ),
3169 "plus1_fulfilled_count" = (
3170 SELECT coalesce(sum("snapshot"."weight"), 0)
3171 FROM "issue" CROSS JOIN "opinion"
3172 JOIN "direct_interest_snapshot" AS "snapshot"
3173 ON "snapshot"."issue_id" = "issue"."id"
3174 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3175 AND "snapshot"."member_id" = "opinion"."member_id"
3176 WHERE "issue"."id" = "issue_id_p"
3177 AND "opinion"."suggestion_id" = "suggestion_id_v"
3178 AND "opinion"."degree" = 1
3179 AND "opinion"."fulfilled" = TRUE
3180 ),
3181 "plus2_unfulfilled_count" = (
3182 SELECT coalesce(sum("snapshot"."weight"), 0)
3183 FROM "issue" CROSS JOIN "opinion"
3184 JOIN "direct_interest_snapshot" AS "snapshot"
3185 ON "snapshot"."issue_id" = "issue"."id"
3186 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3187 AND "snapshot"."member_id" = "opinion"."member_id"
3188 WHERE "issue"."id" = "issue_id_p"
3189 AND "opinion"."suggestion_id" = "suggestion_id_v"
3190 AND "opinion"."degree" = 2
3191 AND "opinion"."fulfilled" = FALSE
3192 ),
3193 "plus2_fulfilled_count" = (
3194 SELECT coalesce(sum("snapshot"."weight"), 0)
3195 FROM "issue" CROSS JOIN "opinion"
3196 JOIN "direct_interest_snapshot" AS "snapshot"
3197 ON "snapshot"."issue_id" = "issue"."id"
3198 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3199 AND "snapshot"."member_id" = "opinion"."member_id"
3200 WHERE "issue"."id" = "issue_id_p"
3201 AND "opinion"."suggestion_id" = "suggestion_id_v"
3202 AND "opinion"."degree" = 2
3203 AND "opinion"."fulfilled" = TRUE
3204 )
3205 WHERE "suggestion"."id" = "suggestion_id_v";
3206 END LOOP;
3207 END LOOP;
3208 RETURN;
3209 END;
3210 $$;
3212 COMMENT ON FUNCTION "create_snapshot"
3213 ( "issue"."id"%TYPE )
3214 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.';
3217 CREATE FUNCTION "set_snapshot_event"
3218 ( "issue_id_p" "issue"."id"%TYPE,
3219 "event_p" "snapshot_event" )
3220 RETURNS VOID
3221 LANGUAGE 'plpgsql' VOLATILE AS $$
3222 DECLARE
3223 "event_v" "issue"."latest_snapshot_event"%TYPE;
3224 BEGIN
3225 SELECT "latest_snapshot_event" INTO "event_v" FROM "issue"
3226 WHERE "id" = "issue_id_p" FOR UPDATE;
3227 UPDATE "issue" SET "latest_snapshot_event" = "event_p"
3228 WHERE "id" = "issue_id_p";
3229 UPDATE "direct_population_snapshot" SET "event" = "event_p"
3230 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3231 UPDATE "delegating_population_snapshot" SET "event" = "event_p"
3232 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3233 UPDATE "direct_interest_snapshot" SET "event" = "event_p"
3234 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3235 UPDATE "delegating_interest_snapshot" SET "event" = "event_p"
3236 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3237 UPDATE "direct_supporter_snapshot" SET "event" = "event_p"
3238 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3239 RETURN;
3240 END;
3241 $$;
3243 COMMENT ON FUNCTION "set_snapshot_event"
3244 ( "issue"."id"%TYPE,
3245 "snapshot_event" )
3246 IS 'Change "event" attribute of the previous ''periodic'' snapshot';
3250 ---------------------
3251 -- Freezing issues --
3252 ---------------------
3254 CREATE FUNCTION "freeze_after_snapshot"
3255 ( "issue_id_p" "issue"."id"%TYPE )
3256 RETURNS VOID
3257 LANGUAGE 'plpgsql' VOLATILE AS $$
3258 DECLARE
3259 "issue_row" "issue"%ROWTYPE;
3260 "policy_row" "policy"%ROWTYPE;
3261 "initiative_row" "initiative"%ROWTYPE;
3262 BEGIN
3263 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3264 SELECT * INTO "policy_row"
3265 FROM "policy" WHERE "id" = "issue_row"."policy_id";
3266 PERFORM "set_snapshot_event"("issue_id_p", 'full_freeze');
3267 FOR "initiative_row" IN
3268 SELECT * FROM "initiative"
3269 WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
3270 LOOP
3271 IF
3272 "initiative_row"."satisfied_supporter_count" > 0 AND
3273 "initiative_row"."satisfied_supporter_count" *
3274 "policy_row"."initiative_quorum_den" >=
3275 "issue_row"."population" * "policy_row"."initiative_quorum_num"
3276 THEN
3277 UPDATE "initiative" SET "admitted" = TRUE
3278 WHERE "id" = "initiative_row"."id";
3279 ELSE
3280 UPDATE "initiative" SET "admitted" = FALSE
3281 WHERE "id" = "initiative_row"."id";
3282 END IF;
3283 END LOOP;
3284 IF EXISTS (
3285 SELECT NULL FROM "initiative"
3286 WHERE "issue_id" = "issue_id_p" AND "admitted" = TRUE
3287 ) THEN
3288 UPDATE "issue" SET
3289 "state" = 'voting',
3290 "accepted" = coalesce("accepted", now()),
3291 "half_frozen" = coalesce("half_frozen", now()),
3292 "fully_frozen" = now()
3293 WHERE "id" = "issue_id_p";
3294 ELSE
3295 UPDATE "issue" SET
3296 "state" = 'canceled_no_initiative_admitted',
3297 "accepted" = coalesce("accepted", now()),
3298 "half_frozen" = coalesce("half_frozen", now()),
3299 "fully_frozen" = now(),
3300 "closed" = now(),
3301 "ranks_available" = TRUE
3302 WHERE "id" = "issue_id_p";
3303 -- NOTE: The following DELETE statements have effect only when
3304 -- issue state has been manipulated
3305 DELETE FROM "direct_voter" WHERE "issue_id" = "issue_id_p";
3306 DELETE FROM "delegating_voter" WHERE "issue_id" = "issue_id_p";
3307 DELETE FROM "battle" WHERE "issue_id" = "issue_id_p";
3308 END IF;
3309 RETURN;
3310 END;
3311 $$;
3313 COMMENT ON FUNCTION "freeze_after_snapshot"
3314 ( "issue"."id"%TYPE )
3315 IS 'This function freezes an issue (fully) and starts voting, but must only be called when "create_snapshot" was called in the same transaction.';
3318 CREATE FUNCTION "manual_freeze"("issue_id_p" "issue"."id"%TYPE)
3319 RETURNS VOID
3320 LANGUAGE 'plpgsql' VOLATILE AS $$
3321 DECLARE
3322 "issue_row" "issue"%ROWTYPE;
3323 BEGIN
3324 PERFORM "create_snapshot"("issue_id_p");
3325 PERFORM "freeze_after_snapshot"("issue_id_p");
3326 RETURN;
3327 END;
3328 $$;
3330 COMMENT ON FUNCTION "manual_freeze"
3331 ( "issue"."id"%TYPE )
3332 IS 'Freeze an issue manually (fully) and start voting';
3336 -----------------------
3337 -- Counting of votes --
3338 -----------------------
3341 CREATE FUNCTION "weight_of_added_vote_delegations"
3342 ( "issue_id_p" "issue"."id"%TYPE,
3343 "member_id_p" "member"."id"%TYPE,
3344 "delegate_member_ids_p" "delegating_voter"."delegate_member_ids"%TYPE )
3345 RETURNS "direct_voter"."weight"%TYPE
3346 LANGUAGE 'plpgsql' VOLATILE AS $$
3347 DECLARE
3348 "issue_delegation_row" "issue_delegation"%ROWTYPE;
3349 "delegate_member_ids_v" "delegating_voter"."delegate_member_ids"%TYPE;
3350 "weight_v" INT4;
3351 "sub_weight_v" INT4;
3352 BEGIN
3353 "weight_v" := 0;
3354 FOR "issue_delegation_row" IN
3355 SELECT * FROM "issue_delegation"
3356 WHERE "trustee_id" = "member_id_p"
3357 AND "issue_id" = "issue_id_p"
3358 LOOP
3359 IF NOT EXISTS (
3360 SELECT NULL FROM "direct_voter"
3361 WHERE "member_id" = "issue_delegation_row"."truster_id"
3362 AND "issue_id" = "issue_id_p"
3363 ) AND NOT EXISTS (
3364 SELECT NULL FROM "delegating_voter"
3365 WHERE "member_id" = "issue_delegation_row"."truster_id"
3366 AND "issue_id" = "issue_id_p"
3367 ) THEN
3368 "delegate_member_ids_v" :=
3369 "member_id_p" || "delegate_member_ids_p";
3370 INSERT INTO "delegating_voter" (
3371 "issue_id",
3372 "member_id",
3373 "scope",
3374 "delegate_member_ids"
3375 ) VALUES (
3376 "issue_id_p",
3377 "issue_delegation_row"."truster_id",
3378 "issue_delegation_row"."scope",
3379 "delegate_member_ids_v"
3380 );
3381 "sub_weight_v" := 1 +
3382 "weight_of_added_vote_delegations"(
3383 "issue_id_p",
3384 "issue_delegation_row"."truster_id",
3385 "delegate_member_ids_v"
3386 );
3387 UPDATE "delegating_voter"
3388 SET "weight" = "sub_weight_v"
3389 WHERE "issue_id" = "issue_id_p"
3390 AND "member_id" = "issue_delegation_row"."truster_id";
3391 "weight_v" := "weight_v" + "sub_weight_v";
3392 END IF;
3393 END LOOP;
3394 RETURN "weight_v";
3395 END;
3396 $$;
3398 COMMENT ON FUNCTION "weight_of_added_vote_delegations"
3399 ( "issue"."id"%TYPE,
3400 "member"."id"%TYPE,
3401 "delegating_voter"."delegate_member_ids"%TYPE )
3402 IS 'Helper function for "add_vote_delegations" function';
3405 CREATE FUNCTION "add_vote_delegations"
3406 ( "issue_id_p" "issue"."id"%TYPE )
3407 RETURNS VOID
3408 LANGUAGE 'plpgsql' VOLATILE AS $$
3409 DECLARE
3410 "member_id_v" "member"."id"%TYPE;
3411 BEGIN
3412 FOR "member_id_v" IN
3413 SELECT "member_id" FROM "direct_voter"
3414 WHERE "issue_id" = "issue_id_p"
3415 LOOP
3416 UPDATE "direct_voter" SET
3417 "weight" = "weight" + "weight_of_added_vote_delegations"(
3418 "issue_id_p",
3419 "member_id_v",
3420 '{}'
3421 )
3422 WHERE "member_id" = "member_id_v"
3423 AND "issue_id" = "issue_id_p";
3424 END LOOP;
3425 RETURN;
3426 END;
3427 $$;
3429 COMMENT ON FUNCTION "add_vote_delegations"
3430 ( "issue_id_p" "issue"."id"%TYPE )
3431 IS 'Helper function for "close_voting" function';
3434 CREATE FUNCTION "close_voting"("issue_id_p" "issue"."id"%TYPE)
3435 RETURNS VOID
3436 LANGUAGE 'plpgsql' VOLATILE AS $$
3437 DECLARE
3438 "area_id_v" "area"."id"%TYPE;
3439 "unit_id_v" "unit"."id"%TYPE;
3440 "member_id_v" "member"."id"%TYPE;
3441 BEGIN
3442 PERFORM "lock_issue"("issue_id_p");
3443 SELECT "area_id" INTO "area_id_v" FROM "issue" WHERE "id" = "issue_id_p";
3444 SELECT "unit_id" INTO "unit_id_v" FROM "area" WHERE "id" = "area_id_v";
3445 -- delete delegating votes (in cases of manual reset of issue state):
3446 DELETE FROM "delegating_voter"
3447 WHERE "issue_id" = "issue_id_p";
3448 -- delete votes from non-privileged voters:
3449 DELETE FROM "direct_voter"
3450 USING (
3451 SELECT
3452 "direct_voter"."member_id"
3453 FROM "direct_voter"
3454 JOIN "member" ON "direct_voter"."member_id" = "member"."id"
3455 LEFT JOIN "privilege"
3456 ON "privilege"."unit_id" = "unit_id_v"
3457 AND "privilege"."member_id" = "direct_voter"."member_id"
3458 WHERE "direct_voter"."issue_id" = "issue_id_p" AND (
3459 "member"."active" = FALSE OR
3460 "privilege"."voting_right" ISNULL OR
3461 "privilege"."voting_right" = FALSE
3462 )
3463 ) AS "subquery"
3464 WHERE "direct_voter"."issue_id" = "issue_id_p"
3465 AND "direct_voter"."member_id" = "subquery"."member_id";
3466 -- consider delegations:
3467 UPDATE "direct_voter" SET "weight" = 1
3468 WHERE "issue_id" = "issue_id_p";
3469 PERFORM "add_vote_delegations"("issue_id_p");
3470 -- set voter count and mark issue as being calculated:
3471 UPDATE "issue" SET
3472 "state" = 'calculation',
3473 "closed" = now(),
3474 "voter_count" = (
3475 SELECT coalesce(sum("weight"), 0)
3476 FROM "direct_voter" WHERE "issue_id" = "issue_id_p"
3477 )
3478 WHERE "id" = "issue_id_p";
3479 -- materialize battle_view:
3480 -- NOTE: "closed" column of issue must be set at this point
3481 DELETE FROM "battle" WHERE "issue_id" = "issue_id_p";
3482 INSERT INTO "battle" (
3483 "issue_id",
3484 "winning_initiative_id", "losing_initiative_id",
3485 "count"
3486 ) SELECT
3487 "issue_id",
3488 "winning_initiative_id", "losing_initiative_id",
3489 "count"
3490 FROM "battle_view" WHERE "issue_id" = "issue_id_p";
3491 -- copy "positive_votes" and "negative_votes" from "battle" table:
3492 UPDATE "initiative" SET
3493 "positive_votes" = "battle_win"."count",
3494 "negative_votes" = "battle_lose"."count"
3495 FROM "battle" AS "battle_win", "battle" AS "battle_lose"
3496 WHERE
3497 "battle_win"."issue_id" = "issue_id_p" AND
3498 "battle_win"."winning_initiative_id" = "initiative"."id" AND
3499 "battle_win"."losing_initiative_id" ISNULL AND
3500 "battle_lose"."issue_id" = "issue_id_p" AND
3501 "battle_lose"."losing_initiative_id" = "initiative"."id" AND
3502 "battle_lose"."winning_initiative_id" ISNULL;
3503 END;
3504 $$;
3506 COMMENT ON FUNCTION "close_voting"
3507 ( "issue"."id"%TYPE )
3508 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.';
3511 CREATE FUNCTION "defeat_strength"
3512 ( "positive_votes_p" INT4, "negative_votes_p" INT4 )
3513 RETURNS INT8
3514 LANGUAGE 'plpgsql' IMMUTABLE AS $$
3515 BEGIN
3516 IF "positive_votes_p" > "negative_votes_p" THEN
3517 RETURN ("positive_votes_p"::INT8 << 31) - "negative_votes_p"::INT8;
3518 ELSIF "positive_votes_p" = "negative_votes_p" THEN
3519 RETURN 0;
3520 ELSE
3521 RETURN -1;
3522 END IF;
3523 END;
3524 $$;
3526 COMMENT ON FUNCTION "defeat_strength"(INT4, INT4) IS 'Calculates defeat strength (INT8!) of a pairwise defeat primarily by the absolute number of votes for the winner and secondarily by the absolute number of votes for the loser';
3529 CREATE FUNCTION "calculate_ranks"("issue_id_p" "issue"."id"%TYPE)
3530 RETURNS VOID
3531 LANGUAGE 'plpgsql' VOLATILE AS $$
3532 DECLARE
3533 "issue_row" "issue"%ROWTYPE;
3534 "policy_row" "policy"%ROWTYPE;
3535 "dimension_v" INTEGER;
3536 "vote_matrix" INT4[][]; -- absolute votes
3537 "matrix" INT8[][]; -- defeat strength / best paths
3538 "i" INTEGER;
3539 "j" INTEGER;
3540 "k" INTEGER;
3541 "battle_row" "battle"%ROWTYPE;
3542 "rank_ary" INT4[];
3543 "rank_v" INT4;
3544 "done_v" INTEGER;
3545 "winners_ary" INTEGER[];
3546 "initiative_id_v" "initiative"."id"%TYPE;
3547 BEGIN
3548 SELECT * INTO "issue_row"
3549 FROM "issue" WHERE "id" = "issue_id_p"
3550 FOR UPDATE;
3551 SELECT * INTO "policy_row"
3552 FROM "policy" WHERE "id" = "issue_row"."policy_id";
3553 SELECT count(1) INTO "dimension_v"
3554 FROM "battle_participant" WHERE "issue_id" = "issue_id_p";
3555 -- Create "vote_matrix" with absolute number of votes in pairwise
3556 -- comparison:
3557 "vote_matrix" := array_fill(NULL::INT4, ARRAY["dimension_v", "dimension_v"]);
3558 "i" := 1;
3559 "j" := 2;
3560 FOR "battle_row" IN
3561 SELECT * FROM "battle" WHERE "issue_id" = "issue_id_p"
3562 ORDER BY
3563 "winning_initiative_id" NULLS LAST,
3564 "losing_initiative_id" NULLS LAST
3565 LOOP
3566 "vote_matrix"["i"]["j"] := "battle_row"."count";
3567 IF "j" = "dimension_v" THEN
3568 "i" := "i" + 1;
3569 "j" := 1;
3570 ELSE
3571 "j" := "j" + 1;
3572 IF "j" = "i" THEN
3573 "j" := "j" + 1;
3574 END IF;
3575 END IF;
3576 END LOOP;
3577 IF "i" != "dimension_v" OR "j" != "dimension_v" + 1 THEN
3578 RAISE EXCEPTION 'Wrong battle count (should not happen)';
3579 END IF;
3580 -- Store defeat strengths in "matrix" using "defeat_strength"
3581 -- function:
3582 "matrix" := array_fill(NULL::INT8, ARRAY["dimension_v", "dimension_v"]);
3583 "i" := 1;
3584 LOOP
3585 "j" := 1;
3586 LOOP
3587 IF "i" != "j" THEN
3588 "matrix"["i"]["j"] := "defeat_strength"(
3589 "vote_matrix"["i"]["j"],
3590 "vote_matrix"["j"]["i"]
3591 );
3592 END IF;
3593 EXIT WHEN "j" = "dimension_v";
3594 "j" := "j" + 1;
3595 END LOOP;
3596 EXIT WHEN "i" = "dimension_v";
3597 "i" := "i" + 1;
3598 END LOOP;
3599 -- Find best paths:
3600 "i" := 1;
3601 LOOP
3602 "j" := 1;
3603 LOOP
3604 IF "i" != "j" THEN
3605 "k" := 1;
3606 LOOP
3607 IF "i" != "k" AND "j" != "k" THEN
3608 IF "matrix"["j"]["i"] < "matrix"["i"]["k"] THEN
3609 IF "matrix"["j"]["i"] > "matrix"["j"]["k"] THEN
3610 "matrix"["j"]["k"] := "matrix"["j"]["i"];
3611 END IF;
3612 ELSE
3613 IF "matrix"["i"]["k"] > "matrix"["j"]["k"] THEN
3614 "matrix"["j"]["k"] := "matrix"["i"]["k"];
3615 END IF;
3616 END IF;
3617 END IF;
3618 EXIT WHEN "k" = "dimension_v";
3619 "k" := "k" + 1;
3620 END LOOP;
3621 END IF;
3622 EXIT WHEN "j" = "dimension_v";
3623 "j" := "j" + 1;
3624 END LOOP;
3625 EXIT WHEN "i" = "dimension_v";
3626 "i" := "i" + 1;
3627 END LOOP;
3628 -- Determine order of winners:
3629 "rank_ary" := array_fill(NULL::INT4, ARRAY["dimension_v"]);
3630 "rank_v" := 1;
3631 "done_v" := 0;
3632 LOOP
3633 "winners_ary" := '{}';
3634 "i" := 1;
3635 LOOP
3636 IF "rank_ary"["i"] ISNULL THEN
3637 "j" := 1;
3638 LOOP
3639 IF
3640 "i" != "j" AND
3641 "rank_ary"["j"] ISNULL AND
3642 "matrix"["j"]["i"] > "matrix"["i"]["j"]
3643 THEN
3644 -- someone else is better
3645 EXIT;
3646 END IF;
3647 IF "j" = "dimension_v" THEN
3648 -- noone is better
3649 "winners_ary" := "winners_ary" || "i";
3650 EXIT;
3651 END IF;
3652 "j" := "j" + 1;
3653 END LOOP;
3654 END IF;
3655 EXIT WHEN "i" = "dimension_v";
3656 "i" := "i" + 1;
3657 END LOOP;
3658 "i" := 1;
3659 LOOP
3660 "rank_ary"["winners_ary"["i"]] := "rank_v";
3661 "done_v" := "done_v" + 1;
3662 EXIT WHEN "i" = array_upper("winners_ary", 1);
3663 "i" := "i" + 1;
3664 END LOOP;
3665 EXIT WHEN "done_v" = "dimension_v";
3666 "rank_v" := "rank_v" + 1;
3667 END LOOP;
3668 -- write preliminary results:
3669 "i" := 1;
3670 FOR "initiative_id_v" IN
3671 SELECT "id" FROM "initiative"
3672 WHERE "issue_id" = "issue_id_p" AND "admitted"
3673 ORDER BY "id"
3674 LOOP
3675 UPDATE "initiative" SET
3676 "direct_majority" =
3677 CASE WHEN "policy_row"."direct_majority_strict" THEN
3678 "positive_votes" * "policy_row"."direct_majority_den" >
3679 "policy_row"."direct_majority_num" * ("positive_votes"+"negative_votes")
3680 ELSE
3681 "positive_votes" * "policy_row"."direct_majority_den" >=
3682 "policy_row"."direct_majority_num" * ("positive_votes"+"negative_votes")
3683 END
3684 AND "positive_votes" >= "policy_row"."direct_majority_positive"
3685 AND "issue_row"."voter_count"-"negative_votes" >=
3686 "policy_row"."direct_majority_non_negative",
3687 "indirect_majority" =
3688 CASE WHEN "policy_row"."indirect_majority_strict" THEN
3689 "positive_votes" * "policy_row"."indirect_majority_den" >
3690 "policy_row"."indirect_majority_num" * ("positive_votes"+"negative_votes")
3691 ELSE
3692 "positive_votes" * "policy_row"."indirect_majority_den" >=
3693 "policy_row"."indirect_majority_num" * ("positive_votes"+"negative_votes")
3694 END
3695 AND "positive_votes" >= "policy_row"."indirect_majority_positive"
3696 AND "issue_row"."voter_count"-"negative_votes" >=
3697 "policy_row"."indirect_majority_non_negative",
3698 "schulze_rank" = "rank_ary"["i"],
3699 "better_than_status_quo" = "rank_ary"["i"] < "rank_ary"["dimension_v"],
3700 "worse_than_status_quo" = "rank_ary"["i"] > "rank_ary"["dimension_v"],
3701 "multistage_majority" = "rank_ary"["i"] >= "rank_ary"["dimension_v"],
3702 "reverse_beat_path" = "matrix"["dimension_v"]["i"] >= 0,
3703 "winner" = FALSE
3704 WHERE "id" = "initiative_id_v";
3705 "i" := "i" + 1;
3706 END LOOP;
3707 IF "i" != "dimension_v" THEN
3708 RAISE EXCEPTION 'Wrong winner count (should not happen)';
3709 END IF;
3710 -- take indirect majorities into account:
3711 LOOP
3712 UPDATE "initiative" SET "indirect_majority" = TRUE
3713 FROM (
3714 SELECT "new_initiative"."id" AS "initiative_id"
3715 FROM "initiative" "old_initiative"
3716 JOIN "initiative" "new_initiative"
3717 ON "new_initiative"."issue_id" = "issue_id_p"
3718 AND "new_initiative"."indirect_majority" = FALSE
3719 JOIN "battle" "battle_win"
3720 ON "battle_win"."issue_id" = "issue_id_p"
3721 AND "battle_win"."winning_initiative_id" = "new_initiative"."id"
3722 AND "battle_win"."losing_initiative_id" = "old_initiative"."id"
3723 JOIN "battle" "battle_lose"
3724 ON "battle_lose"."issue_id" = "issue_id_p"
3725 AND "battle_lose"."losing_initiative_id" = "new_initiative"."id"
3726 AND "battle_lose"."winning_initiative_id" = "old_initiative"."id"
3727 WHERE "old_initiative"."issue_id" = "issue_id_p"
3728 AND "old_initiative"."indirect_majority" = TRUE
3729 AND CASE WHEN "policy_row"."indirect_majority_strict" THEN
3730 "battle_win"."count" * "policy_row"."indirect_majority_den" >
3731 "policy_row"."indirect_majority_num" *
3732 ("battle_win"."count"+"battle_lose"."count")
3733 ELSE
3734 "battle_win"."count" * "policy_row"."indirect_majority_den" >=
3735 "policy_row"."indirect_majority_num" *
3736 ("battle_win"."count"+"battle_lose"."count")
3737 END
3738 AND "battle_win"."count" >= "policy_row"."indirect_majority_positive"
3739 AND "issue_row"."voter_count"-"battle_lose"."count" >=
3740 "policy_row"."indirect_majority_non_negative"
3741 ) AS "subquery"
3742 WHERE "id" = "subquery"."initiative_id";
3743 EXIT WHEN NOT FOUND;
3744 END LOOP;
3745 -- set "multistage_majority" for remaining matching initiatives:
3746 UPDATE "initiative" SET "multistage_majority" = TRUE
3747 FROM (
3748 SELECT "losing_initiative"."id" AS "initiative_id"
3749 FROM "initiative" "losing_initiative"
3750 JOIN "initiative" "winning_initiative"
3751 ON "winning_initiative"."issue_id" = "issue_id_p"
3752 AND "winning_initiative"."admitted"
3753 JOIN "battle" "battle_win"
3754 ON "battle_win"."issue_id" = "issue_id_p"
3755 AND "battle_win"."winning_initiative_id" = "winning_initiative"."id"
3756 AND "battle_win"."losing_initiative_id" = "losing_initiative"."id"
3757 JOIN "battle" "battle_lose"
3758 ON "battle_lose"."issue_id" = "issue_id_p"
3759 AND "battle_lose"."losing_initiative_id" = "winning_initiative"."id"
3760 AND "battle_lose"."winning_initiative_id" = "losing_initiative"."id"
3761 WHERE "losing_initiative"."issue_id" = "issue_id_p"
3762 AND "losing_initiative"."admitted"
3763 AND "winning_initiative"."schulze_rank" <
3764 "losing_initiative"."schulze_rank"
3765 AND "battle_win"."count" > "battle_lose"."count"
3766 AND (
3767 "battle_win"."count" > "winning_initiative"."positive_votes" OR
3768 "battle_lose"."count" < "losing_initiative"."negative_votes" )
3769 ) AS "subquery"
3770 WHERE "id" = "subquery"."initiative_id";
3771 -- mark eligible initiatives:
3772 UPDATE "initiative" SET "eligible" = TRUE
3773 WHERE "issue_id" = "issue_id_p"
3774 AND "initiative"."direct_majority"
3775 AND "initiative"."indirect_majority"
3776 AND "initiative"."better_than_status_quo"
3777 AND (
3778 "policy_row"."no_multistage_majority" = FALSE OR
3779 "initiative"."multistage_majority" = FALSE )
3780 AND (
3781 "policy_row"."no_reverse_beat_path" = FALSE OR
3782 "initiative"."reverse_beat_path" = FALSE );
3783 -- mark final winner:
3784 UPDATE "initiative" SET "winner" = TRUE
3785 FROM (
3786 SELECT "id" AS "initiative_id"
3787 FROM "initiative"
3788 WHERE "issue_id" = "issue_id_p" AND "eligible"
3789 ORDER BY "schulze_rank", "id"
3790 LIMIT 1
3791 ) AS "subquery"
3792 WHERE "id" = "subquery"."initiative_id";
3793 -- write (final) ranks:
3794 "rank_v" := 1;
3795 FOR "initiative_id_v" IN
3796 SELECT "id"
3797 FROM "initiative"
3798 WHERE "issue_id" = "issue_id_p" AND "admitted"
3799 ORDER BY
3800 "winner" DESC,
3801 ("direct_majority" AND "indirect_majority") DESC,
3802 "schulze_rank",
3803 "id"
3804 LOOP
3805 UPDATE "initiative" SET "rank" = "rank_v"
3806 WHERE "id" = "initiative_id_v";
3807 "rank_v" := "rank_v" + 1;
3808 END LOOP;
3809 -- set schulze rank of status quo and mark issue as finished:
3810 UPDATE "issue" SET
3811 "status_quo_schulze_rank" = "rank_ary"["dimension_v"],
3812 "state" =
3813 CASE WHEN EXISTS (
3814 SELECT NULL FROM "initiative"
3815 WHERE "issue_id" = "issue_id_p" AND "winner"
3816 ) THEN
3817 'finished_with_winner'::"issue_state"
3818 ELSE
3819 'finished_without_winner'::"issue_state"
3820 END,
3821 "ranks_available" = TRUE
3822 WHERE "id" = "issue_id_p";
3823 RETURN;
3824 END;
3825 $$;
3827 COMMENT ON FUNCTION "calculate_ranks"
3828 ( "issue"."id"%TYPE )
3829 IS 'Determine ranking (Votes have to be counted first)';
3833 -----------------------------
3834 -- Automatic state changes --
3835 -----------------------------
3838 CREATE FUNCTION "check_issue"
3839 ( "issue_id_p" "issue"."id"%TYPE )
3840 RETURNS VOID
3841 LANGUAGE 'plpgsql' VOLATILE AS $$
3842 DECLARE
3843 "issue_row" "issue"%ROWTYPE;
3844 "policy_row" "policy"%ROWTYPE;
3845 BEGIN
3846 PERFORM "lock_issue"("issue_id_p");
3847 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3848 -- only process open issues:
3849 IF "issue_row"."closed" ISNULL THEN
3850 SELECT * INTO "policy_row" FROM "policy"
3851 WHERE "id" = "issue_row"."policy_id";
3852 -- create a snapshot, unless issue is already fully frozen:
3853 IF "issue_row"."fully_frozen" ISNULL THEN
3854 PERFORM "create_snapshot"("issue_id_p");
3855 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3856 END IF;
3857 -- eventually close or accept issues, which have not been accepted:
3858 IF "issue_row"."accepted" ISNULL THEN
3859 IF EXISTS (
3860 SELECT NULL FROM "initiative"
3861 WHERE "issue_id" = "issue_id_p"
3862 AND "supporter_count" > 0
3863 AND "supporter_count" * "policy_row"."issue_quorum_den"
3864 >= "issue_row"."population" * "policy_row"."issue_quorum_num"
3865 ) THEN
3866 -- accept issues, if supporter count is high enough
3867 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
3868 -- NOTE: "issue_row" used later
3869 "issue_row"."state" := 'discussion';
3870 "issue_row"."accepted" := now();
3871 UPDATE "issue" SET
3872 "state" = "issue_row"."state",
3873 "accepted" = "issue_row"."accepted"
3874 WHERE "id" = "issue_row"."id";
3875 ELSIF
3876 now() >= "issue_row"."created" + "issue_row"."admission_time"
3877 THEN
3878 -- close issues, if admission time has expired
3879 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
3880 UPDATE "issue" SET
3881 "state" = 'canceled_issue_not_accepted',
3882 "closed" = now()
3883 WHERE "id" = "issue_row"."id";
3884 END IF;
3885 END IF;
3886 -- eventually half freeze issues:
3887 IF
3888 -- NOTE: issue can't be closed at this point, if it has been accepted
3889 "issue_row"."accepted" NOTNULL AND
3890 "issue_row"."half_frozen" ISNULL
3891 THEN
3892 IF
3893 now() >= "issue_row"."accepted" + "issue_row"."discussion_time"
3894 THEN
3895 PERFORM "set_snapshot_event"("issue_id_p", 'half_freeze');
3896 -- NOTE: "issue_row" used later
3897 "issue_row"."state" := 'verification';
3898 "issue_row"."half_frozen" := now();
3899 UPDATE "issue" SET
3900 "state" = "issue_row"."state",
3901 "half_frozen" = "issue_row"."half_frozen"
3902 WHERE "id" = "issue_row"."id";
3903 END IF;
3904 END IF;
3905 -- close issues after some time, if all initiatives have been revoked:
3906 IF
3907 "issue_row"."closed" ISNULL AND
3908 NOT EXISTS (
3909 -- all initiatives are revoked
3910 SELECT NULL FROM "initiative"
3911 WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
3912 ) AND (
3913 -- and issue has not been accepted yet
3914 "issue_row"."accepted" ISNULL OR
3915 NOT EXISTS (
3916 -- or no initiatives have been revoked lately
3917 SELECT NULL FROM "initiative"
3918 WHERE "issue_id" = "issue_id_p"
3919 AND now() < "revoked" + "issue_row"."verification_time"
3920 ) OR (
3921 -- or verification time has elapsed
3922 "issue_row"."half_frozen" NOTNULL AND
3923 "issue_row"."fully_frozen" ISNULL AND
3924 now() >= "issue_row"."half_frozen" + "issue_row"."verification_time"
3925 )
3926 )
3927 THEN
3928 -- NOTE: "issue_row" used later
3929 IF "issue_row"."accepted" ISNULL THEN
3930 "issue_row"."state" := 'canceled_revoked_before_accepted';
3931 ELSIF "issue_row"."half_frozen" ISNULL THEN
3932 "issue_row"."state" := 'canceled_after_revocation_during_discussion';
3933 ELSE
3934 "issue_row"."state" := 'canceled_after_revocation_during_verification';
3935 END IF;
3936 "issue_row"."closed" := now();
3937 UPDATE "issue" SET
3938 "state" = "issue_row"."state",
3939 "closed" = "issue_row"."closed"
3940 WHERE "id" = "issue_row"."id";
3941 END IF;
3942 -- fully freeze issue after verification time:
3943 IF
3944 "issue_row"."half_frozen" NOTNULL AND
3945 "issue_row"."fully_frozen" ISNULL AND
3946 "issue_row"."closed" ISNULL AND
3947 now() >= "issue_row"."half_frozen" + "issue_row"."verification_time"
3948 THEN
3949 PERFORM "freeze_after_snapshot"("issue_id_p");
3950 -- NOTE: "issue" might change, thus "issue_row" has to be updated below
3951 END IF;
3952 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3953 -- close issue by calling close_voting(...) after voting time:
3954 IF
3955 "issue_row"."closed" ISNULL AND
3956 "issue_row"."fully_frozen" NOTNULL AND
3957 now() >= "issue_row"."fully_frozen" + "issue_row"."voting_time"
3958 THEN
3959 PERFORM "close_voting"("issue_id_p");
3960 -- calculate ranks will not consume much time and can be done now
3961 PERFORM "calculate_ranks"("issue_id_p");
3962 END IF;
3963 END IF;
3964 RETURN;
3965 END;
3966 $$;
3968 COMMENT ON FUNCTION "check_issue"
3969 ( "issue"."id"%TYPE )
3970 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.';
3973 CREATE FUNCTION "check_everything"()
3974 RETURNS VOID
3975 LANGUAGE 'plpgsql' VOLATILE AS $$
3976 DECLARE
3977 "issue_id_v" "issue"."id"%TYPE;
3978 BEGIN
3979 DELETE FROM "expired_session";
3980 PERFORM "check_last_login"();
3981 PERFORM "calculate_member_counts"();
3982 FOR "issue_id_v" IN SELECT "id" FROM "open_issue" LOOP
3983 PERFORM "check_issue"("issue_id_v");
3984 END LOOP;
3985 FOR "issue_id_v" IN SELECT "id" FROM "issue_with_ranks_missing" LOOP
3986 PERFORM "calculate_ranks"("issue_id_v");
3987 END LOOP;
3988 RETURN;
3989 END;
3990 $$;
3992 COMMENT ON FUNCTION "check_everything"() IS 'Amongst other regular tasks this function performs "check_issue" for every open issue, and if possible, automatically calculates ranks. Use this function only for development and debugging purposes, as long transactions with exclusive locking may result. In productive environments you should call the lf_update program instead.';
3996 ----------------------
3997 -- Deletion of data --
3998 ----------------------
4001 CREATE FUNCTION "clean_issue"("issue_id_p" "issue"."id"%TYPE)
4002 RETURNS VOID
4003 LANGUAGE 'plpgsql' VOLATILE AS $$
4004 DECLARE
4005 "issue_row" "issue"%ROWTYPE;
4006 BEGIN
4007 SELECT * INTO "issue_row"
4008 FROM "issue" WHERE "id" = "issue_id_p"
4009 FOR UPDATE;
4010 IF "issue_row"."cleaned" ISNULL THEN
4011 UPDATE "issue" SET
4012 "state" = 'voting',
4013 "closed" = NULL,
4014 "ranks_available" = FALSE
4015 WHERE "id" = "issue_id_p";
4016 DELETE FROM "issue_comment"
4017 WHERE "issue_id" = "issue_id_p";
4018 DELETE FROM "voting_comment"
4019 WHERE "issue_id" = "issue_id_p";
4020 DELETE FROM "delegating_voter"
4021 WHERE "issue_id" = "issue_id_p";
4022 DELETE FROM "direct_voter"
4023 WHERE "issue_id" = "issue_id_p";
4024 DELETE FROM "delegating_interest_snapshot"
4025 WHERE "issue_id" = "issue_id_p";
4026 DELETE FROM "direct_interest_snapshot"
4027 WHERE "issue_id" = "issue_id_p";
4028 DELETE FROM "delegating_population_snapshot"
4029 WHERE "issue_id" = "issue_id_p";
4030 DELETE FROM "direct_population_snapshot"
4031 WHERE "issue_id" = "issue_id_p";
4032 DELETE FROM "non_voter"
4033 WHERE "issue_id" = "issue_id_p";
4034 DELETE FROM "delegation"
4035 WHERE "issue_id" = "issue_id_p";
4036 DELETE FROM "supporter"
4037 WHERE "issue_id" = "issue_id_p";
4038 UPDATE "issue" SET
4039 "state" = "issue_row"."state",
4040 "closed" = "issue_row"."closed",
4041 "ranks_available" = "issue_row"."ranks_available",
4042 "cleaned" = now()
4043 WHERE "id" = "issue_id_p";
4044 END IF;
4045 RETURN;
4046 END;
4047 $$;
4049 COMMENT ON FUNCTION "clean_issue"("issue"."id"%TYPE) IS 'Delete discussion data and votes belonging to an issue';
4052 CREATE FUNCTION "delete_member"("member_id_p" "member"."id"%TYPE)
4053 RETURNS VOID
4054 LANGUAGE 'plpgsql' VOLATILE AS $$
4055 BEGIN
4056 UPDATE "member" SET
4057 "last_login" = NULL,
4058 "last_login_public" = NULL,
4059 "login" = NULL,
4060 "password" = NULL,
4061 "locked" = TRUE,
4062 "active" = FALSE,
4063 "notify_email" = NULL,
4064 "notify_email_unconfirmed" = NULL,
4065 "notify_email_secret" = NULL,
4066 "notify_email_secret_expiry" = NULL,
4067 "notify_email_lock_expiry" = NULL,
4068 "password_reset_secret" = NULL,
4069 "password_reset_secret_expiry" = NULL,
4070 "organizational_unit" = NULL,
4071 "internal_posts" = NULL,
4072 "realname" = NULL,
4073 "birthday" = NULL,
4074 "address" = NULL,
4075 "email" = NULL,
4076 "xmpp_address" = NULL,
4077 "website" = NULL,
4078 "phone" = NULL,
4079 "mobile_phone" = NULL,
4080 "profession" = NULL,
4081 "external_memberships" = NULL,
4082 "external_posts" = NULL,
4083 "statement" = NULL
4084 WHERE "id" = "member_id_p";
4085 -- "text_search_data" is updated by triggers
4086 DELETE FROM "setting" WHERE "member_id" = "member_id_p";
4087 DELETE FROM "setting_map" WHERE "member_id" = "member_id_p";
4088 DELETE FROM "member_relation_setting" WHERE "member_id" = "member_id_p";
4089 DELETE FROM "member_image" WHERE "member_id" = "member_id_p";
4090 DELETE FROM "contact" WHERE "member_id" = "member_id_p";
4091 DELETE FROM "ignored_member" WHERE "member_id" = "member_id_p";
4092 DELETE FROM "session" WHERE "member_id" = "member_id_p";
4093 DELETE FROM "area_setting" WHERE "member_id" = "member_id_p";
4094 DELETE FROM "issue_setting" WHERE "member_id" = "member_id_p";
4095 DELETE FROM "ignored_initiative" WHERE "member_id" = "member_id_p";
4096 DELETE FROM "initiative_setting" WHERE "member_id" = "member_id_p";
4097 DELETE FROM "suggestion_setting" WHERE "member_id" = "member_id_p";
4098 DELETE FROM "membership" WHERE "member_id" = "member_id_p";
4099 DELETE FROM "delegation" WHERE "truster_id" = "member_id_p";
4100 DELETE FROM "non_voter" WHERE "member_id" = "member_id_p";
4101 DELETE FROM "direct_voter" USING "issue"
4102 WHERE "direct_voter"."issue_id" = "issue"."id"
4103 AND "issue"."closed" ISNULL
4104 AND "member_id" = "member_id_p";
4105 RETURN;
4106 END;
4107 $$;
4109 COMMENT ON FUNCTION "delete_member"("member_id_p" "member"."id"%TYPE) IS 'Deactivate member and clear certain settings and data of this member (data protection)';
4112 CREATE FUNCTION "delete_private_data"()
4113 RETURNS VOID
4114 LANGUAGE 'plpgsql' VOLATILE AS $$
4115 BEGIN
4116 UPDATE "member" SET
4117 "last_login" = NULL,
4118 "login" = NULL,
4119 "password" = NULL,
4120 "notify_email" = NULL,
4121 "notify_email_unconfirmed" = NULL,
4122 "notify_email_secret" = NULL,
4123 "notify_email_secret_expiry" = NULL,
4124 "notify_email_lock_expiry" = NULL,
4125 "password_reset_secret" = NULL,
4126 "password_reset_secret_expiry" = NULL,
4127 "organizational_unit" = NULL,
4128 "internal_posts" = NULL,
4129 "realname" = NULL,
4130 "birthday" = NULL,
4131 "address" = NULL,
4132 "email" = NULL,
4133 "xmpp_address" = NULL,
4134 "website" = NULL,
4135 "phone" = NULL,
4136 "mobile_phone" = NULL,
4137 "profession" = NULL,
4138 "external_memberships" = NULL,
4139 "external_posts" = NULL,
4140 "statement" = NULL;
4141 -- "text_search_data" is updated by triggers
4142 DELETE FROM "invite_code";
4143 DELETE FROM "setting";
4144 DELETE FROM "setting_map";
4145 DELETE FROM "member_relation_setting";
4146 DELETE FROM "member_image";
4147 DELETE FROM "contact";
4148 DELETE FROM "ignored_member";
4149 DELETE FROM "session";
4150 DELETE FROM "area_setting";
4151 DELETE FROM "issue_setting";
4152 DELETE FROM "ignored_initiative";
4153 DELETE FROM "initiative_setting";
4154 DELETE FROM "suggestion_setting";
4155 DELETE FROM "non_voter";
4156 DELETE FROM "direct_voter" USING "issue"
4157 WHERE "direct_voter"."issue_id" = "issue"."id"
4158 AND "issue"."closed" ISNULL;
4159 RETURN;
4160 END;
4161 $$;
4163 COMMENT ON FUNCTION "delete_private_data"() IS 'Used by lf_export script. 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.';
4167 COMMIT;