liquid_feedback_core
view core.sql @ 280:5a32f862b2dd
Unique "client_identifier" for "registered_client", No foreign key in table "authorized_client"
author | jbe |
---|---|
date | Wed Aug 15 11:07:07 2012 +0200 (2012-08-15) |
parents | 03897de17853 |
children | 2fd3ac2f8323 |
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 ('2.1.0', 2, 1, 0))
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 show any activity.';
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 "invite_code_expiry" TIMESTAMPTZ,
89 "admin_comment" TEXT,
90 "activated" TIMESTAMPTZ,
91 "last_activity" DATE,
92 "last_login" TIMESTAMPTZ,
93 "login" TEXT UNIQUE,
94 "password" TEXT,
95 "locked" BOOLEAN NOT NULL DEFAULT FALSE,
96 "active" BOOLEAN NOT NULL DEFAULT FALSE,
97 "admin" BOOLEAN NOT NULL DEFAULT FALSE,
98 "lang" TEXT,
99 "notify_email" TEXT,
100 "notify_email_unconfirmed" TEXT,
101 "notify_email_secret" TEXT UNIQUE,
102 "notify_email_secret_expiry" TIMESTAMPTZ,
103 "notify_email_lock_expiry" TIMESTAMPTZ,
104 "notify_level" "notify_level",
105 "password_reset_secret" TEXT UNIQUE,
106 "password_reset_secret_expiry" TIMESTAMPTZ,
107 "name" TEXT UNIQUE,
108 "identification" TEXT UNIQUE,
109 "authentication" TEXT,
110 "organizational_unit" TEXT,
111 "internal_posts" TEXT,
112 "realname" TEXT,
113 "birthday" DATE,
114 "address" TEXT,
115 "email" TEXT,
116 "xmpp_address" TEXT,
117 "website" TEXT,
118 "phone" TEXT,
119 "mobile_phone" TEXT,
120 "profession" TEXT,
121 "external_memberships" TEXT,
122 "external_posts" TEXT,
123 "formatting_engine" TEXT,
124 "statement" TEXT,
125 "text_search_data" TSVECTOR,
126 CONSTRAINT "active_requires_activated_and_last_activity"
127 CHECK ("active" = FALSE OR ("activated" NOTNULL AND "last_activity" NOTNULL)),
128 CONSTRAINT "name_not_null_if_activated"
129 CHECK ("activated" ISNULL OR "name" NOTNULL) );
130 CREATE INDEX "member_active_idx" ON "member" ("active");
131 CREATE INDEX "member_text_search_data_idx" ON "member" USING gin ("text_search_data");
132 CREATE TRIGGER "update_text_search_data"
133 BEFORE INSERT OR UPDATE ON "member"
134 FOR EACH ROW EXECUTE PROCEDURE
135 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
136 "name", "identification", "organizational_unit", "internal_posts",
137 "realname", "external_memberships", "external_posts", "statement" );
139 COMMENT ON TABLE "member" IS 'Users of the system, e.g. members of an organization';
141 COMMENT ON COLUMN "member"."created" IS 'Creation of member record and/or invite code';
142 COMMENT ON COLUMN "member"."invite_code" IS 'Optional invite code, to allow a member to initialize his/her account the first time';
143 COMMENT ON COLUMN "member"."invite_code_expiry" IS 'Expiry data/time for "invite_code"';
144 COMMENT ON COLUMN "member"."admin_comment" IS 'Hidden comment for administrative purposes';
145 COMMENT ON COLUMN "member"."activated" IS 'Timestamp of first activation of account (i.e. usage of "invite_code"); required to be set for "active" members';
146 COMMENT ON COLUMN "member"."last_activity" IS 'Date of last activity of member; required to be set for "active" members';
147 COMMENT ON COLUMN "member"."last_login" IS 'Timestamp of last login';
148 COMMENT ON COLUMN "member"."login" IS 'Login name';
149 COMMENT ON COLUMN "member"."password" IS 'Password (preferably as crypto-hash, depending on the frontend or access layer)';
150 COMMENT ON COLUMN "member"."locked" IS 'Locked members can not log in.';
151 COMMENT ON COLUMN "member"."active" IS 'Memberships, support and votes are taken into account when corresponding members are marked as active. Automatically set to FALSE, if "last_activity" is older than "system_setting"."member_ttl".';
152 COMMENT ON COLUMN "member"."admin" IS 'TRUE for admins, which can administrate other users and setup policies and areas';
153 COMMENT ON COLUMN "member"."lang" IS 'Language code of the preferred language of the member';
154 COMMENT ON COLUMN "member"."notify_email" IS 'Email address where notifications of the system are sent to';
155 COMMENT ON COLUMN "member"."notify_email_unconfirmed" IS 'Unconfirmed email address provided by the member to be copied into "notify_email" field after verification';
156 COMMENT ON COLUMN "member"."notify_email_secret" IS 'Secret sent to the address in "notify_email_unconformed"';
157 COMMENT ON COLUMN "member"."notify_email_secret_expiry" IS 'Expiry date/time for "notify_email_secret"';
158 COMMENT ON COLUMN "member"."notify_email_lock_expiry" IS 'Date/time until no further email confirmation mails may be sent (abuse protection)';
159 COMMENT ON COLUMN "member"."notify_level" IS 'Selects which event notifications are to be sent to the "notify_email" mail address, may be NULL if member did not make any selection yet';
160 COMMENT ON COLUMN "member"."name" IS 'Distinct name of the member, may be NULL if account has not been activated yet';
161 COMMENT ON COLUMN "member"."identification" IS 'Optional identification number or code of the member';
162 COMMENT ON COLUMN "member"."authentication" IS 'Information about how this member was authenticated';
163 COMMENT ON COLUMN "member"."organizational_unit" IS 'Branch or division of the organization the member belongs to';
164 COMMENT ON COLUMN "member"."internal_posts" IS 'Posts (offices) of the member inside the organization';
165 COMMENT ON COLUMN "member"."realname" IS 'Real name of the member, may be identical with "name"';
166 COMMENT ON COLUMN "member"."email" IS 'Published email address of the member; not used for system notifications';
167 COMMENT ON COLUMN "member"."external_memberships" IS 'Other organizations the member is involved in';
168 COMMENT ON COLUMN "member"."external_posts" IS 'Posts (offices) outside the organization';
169 COMMENT ON COLUMN "member"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used for "member"."statement"';
170 COMMENT ON COLUMN "member"."statement" IS 'Freely chosen text of the member for his/her profile';
173 -- DEPRECATED API TABLES --
175 CREATE TYPE "application_access_level" AS ENUM
176 ('member', 'full', 'pseudonymous', 'anonymous');
178 COMMENT ON TYPE "application_access_level" IS 'DEPRECATED, WILL BE REMOVED! Access privileges for applications using the API';
181 CREATE TABLE "member_application" (
182 "id" SERIAL8 PRIMARY KEY,
183 UNIQUE ("member_id", "name"),
184 "member_id" INT4 NOT NULL REFERENCES "member" ("id")
185 ON DELETE CASCADE ON UPDATE CASCADE,
186 "name" TEXT NOT NULL,
187 "comment" TEXT,
188 "access_level" "application_access_level" NOT NULL,
189 "key" TEXT NOT NULL UNIQUE,
190 "last_usage" TIMESTAMPTZ );
192 COMMENT ON TABLE "member_application" IS 'DEPRECATED, WILL BE REMOVED! Registered application being allowed to use the API';
194 -- END OF DEPRECARED API TABLES --
197 -- NEW PRELIMINARY API TABLES --
199 CREATE TYPE "api_access_level" AS ENUM (
200 'none', 'anonymous', 'authors_pseudonymous', 'all_pseudonymous', 'everything', 'member' );
202 COMMENT ON TYPE "api_access_level" IS 'PRELIMINARY, SUBJECT TO CHANGE! Access scope for API consumers.';
205 CREATE TABLE "registered_client" (
206 "id" SERIAL8 PRIMARY KEY,
207 "name" TEXT NOT NULL,
208 "client_identifier" TEXT NOT NULL UNIQUE,
209 "client_secret" TEXT,
210 "code_grant" BOOLEAN NOT NULL,
211 "implicit_grant" BOOLEAN NOT NULL,
212 "client_grant" BOOLEAN NOT NULL,
213 "access_level" "api_access_level",
214 "client_grant_access_level" "api_access_level",
215 "single_token" BOOLEAN NOT NULL DEFAULT FALSE,
216 "always_authorized" BOOLEAN NOT NULL DEFAULT FALSE,
217 "auth_duration" INTERVAL,
218 "login_duration" INTERVAL,
219 "refresh_duration" INTERVAL,
220 "access_duration" INTERVAL,
221 CONSTRAINT "code_or_implicit_grant_requires_access_level"
222 CHECK (("code_grant"=FALSE AND "implicit_grant"=FALSE) OR "access_level" NOTNULL),
223 CONSTRAINT "client_grant_requires_client_grant_access_level"
224 CHECK ("client_grant"=FALSE OR "client_grant_access_level" NOTNULL) );
226 COMMENT ON TABLE "registered_client" IS 'PRELIMINARY, SUBJECT TO CHANGE! OAuth2 client registered by administrator';
228 COMMENT ON COLUMN "registered_client"."name" IS 'Name of the registered client';
229 COMMENT ON COLUMN "registered_client"."client_identifier" IS 'OAuth2 client id, also used as redirection endpoint if "code_grant" or "implicit_grant" is set to TRUE';
230 COMMENT ON COLUMN "registered_client"."client_secret" IS 'Secret for client authentication';
231 COMMENT ON COLUMN "registered_client"."code_grant" IS 'Enable OAuth2 Authorization Code Grant';
232 COMMENT ON COLUMN "registered_client"."implicit_grant" IS 'Enable OAuth2 Implicit Grant';
233 COMMENT ON COLUMN "registered_client"."client_grant" IS 'Enable OAuth2 Client Credentials Grant';
234 COMMENT ON COLUMN "registered_client"."access_level" IS 'Maximum access level for OAuth2 Authorization Code Grant and Implicit Grant';
235 COMMENT ON COLUMN "registered_client"."client_grant_access_level" IS 'Maximum access level for OAuth2 Client Credentials Grant';
236 COMMENT ON COLUMN "registered_client"."single_token" IS 'Allow only one valid refresh token';
237 COMMENT ON COLUMN "registered_client"."always_authorized" IS 'Members do not need to authorize the client';
238 COMMENT ON COLUMN "registered_client"."auth_duration" IS 'Duration of authorization by member';
239 COMMENT ON COLUMN "registered_client"."login_duration" IS 'Life time of refresh code chain';
240 COMMENT ON COLUMN "registered_client"."refresh_duration" IS 'Life time of a refresh code';
241 COMMENT ON COLUMN "registered_client"."access_duration" IS 'Life time of an access code';
244 CREATE TABLE "authorized_client" (
245 "id" SERIAL8 PRIMARY KEY,
246 UNIQUE ("client_identifier", "member_id"),
247 "client_identifier" TEXT NOT NULL,
248 "member_id" INT4 NOT NULL REFERENCES "member" ("id")
249 ON DELETE CASCADE ON UPDATE CASCADE,
250 "access_level" "api_access_level" NOT NULL,
251 "first_auth" TIMESTAMPTZ NOT NULL,
252 "last_auth" TIMESTAMPTZ NOT NULL,
253 UNIQUE ("client_identifier", "member_id") );
255 COMMENT ON TABLE "authorized_client" IS 'PRELIMINARY, SUBJECT TO CHANGE! OAuth2 client authorized by member, or automatically authorized for a member if "registered_client"."always_authorized" is set';
257 COMMENT ON COLUMN "authorized_client"."client_identifier" IS 'OAuth2 client id, also used as redirection endpoint';
258 COMMENT ON COLUMN "authorized_client"."member_id" IS 'Member who authorized the client';
259 COMMENT ON COLUMN "authorized_client"."access_level" IS 'Authorized access level';
260 COMMENT ON COLUMN "authorized_client"."first_auth" IS 'Date/time of initial authorization';
261 COMMENT ON COLUMN "authorized_client"."last_auth" IS 'Date/time of last authorization refresh';
264 CREATE TABLE "authorized_client_token" (
265 "id" SERIAL8 PRIMARY KEY,
266 "authorized_client_id" INT8 NOT NULL REFERENCES "authorized_client"
267 ON DELETE CASCADE ON UPDATE CASCADE,
268 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
269 "authorization_code" TEXT,
270 "refreshed" TIMESTAMPTZ,
271 "refresh_token" TEXT,
272 "old_refresh_token" TEXT,
273 CONSTRAINT "one_of_authorization_code_and_refresh_token_set"
274 CHECK ("authorization_code" NOTNULL OR "refresh_token" NOTNULL),
275 CONSTRAINT "refresh_token_if_and_only_if_refreshed"
276 CHECK ("refreshed" NOTNULL = "refresh_token" NOTNULL),
277 CONSTRAINT "old_refresh_token_requires_current_refresh_token"
278 CHECK ("refresh_token" NOTNULL OR "old_refresh_token" ISNULL) );
280 COMMENT ON TABLE "authorized_client_token" IS 'PRELIMINARY, SUBJECT TO CHANGE! Issued OAuth2 authorization codes and refresh tokens';
282 COMMENT ON COLUMN "authorized_client_token"."created" IS 'Date/time when authorization code (or first refresh token when there is no authorization code) has been created';
283 COMMENT ON COLUMN "authorized_client_token"."authorization_code" IS 'OAuth2 authorization code (only valid for a very short time after it has been created)';
284 COMMENT ON COLUMN "authorized_client_token"."refreshed" IS 'Date/time of last refresh';
285 COMMENT ON COLUMN "authorized_client_token"."refresh_token" IS 'OAuth2 refresh token';
287 -- END OF NEW PRELIMINARY API TABLES --
290 CREATE TABLE "member_history" (
291 "id" SERIAL8 PRIMARY KEY,
292 "member_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
293 "until" TIMESTAMPTZ NOT NULL DEFAULT now(),
294 "active" BOOLEAN NOT NULL,
295 "name" TEXT NOT NULL );
296 CREATE INDEX "member_history_member_id_idx" ON "member_history" ("member_id");
298 COMMENT ON TABLE "member_history" IS 'Filled by trigger; keeps information about old names and active flag of members';
300 COMMENT ON COLUMN "member_history"."id" IS 'Primary key, which can be used to sort entries correctly (and time warp resistant)';
301 COMMENT ON COLUMN "member_history"."until" IS 'Timestamp until the data was valid';
304 CREATE TABLE "rendered_member_statement" (
305 PRIMARY KEY ("member_id", "format"),
306 "member_id" INT8 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
307 "format" TEXT,
308 "content" TEXT NOT NULL );
310 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)';
313 CREATE TABLE "setting" (
314 PRIMARY KEY ("member_id", "key"),
315 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
316 "key" TEXT NOT NULL,
317 "value" TEXT NOT NULL );
318 CREATE INDEX "setting_key_idx" ON "setting" ("key");
320 COMMENT ON TABLE "setting" IS 'Place to store a frontend specific setting for members as a string';
322 COMMENT ON COLUMN "setting"."key" IS 'Name of the setting, preceded by a frontend specific prefix';
325 CREATE TABLE "setting_map" (
326 PRIMARY KEY ("member_id", "key", "subkey"),
327 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
328 "key" TEXT NOT NULL,
329 "subkey" TEXT NOT NULL,
330 "value" TEXT NOT NULL );
331 CREATE INDEX "setting_map_key_idx" ON "setting_map" ("key");
333 COMMENT ON TABLE "setting_map" IS 'Place to store a frontend specific setting for members as a map of key value pairs';
335 COMMENT ON COLUMN "setting_map"."key" IS 'Name of the setting, preceded by a frontend specific prefix';
336 COMMENT ON COLUMN "setting_map"."subkey" IS 'Key of a map entry';
337 COMMENT ON COLUMN "setting_map"."value" IS 'Value of a map entry';
340 CREATE TABLE "member_relation_setting" (
341 PRIMARY KEY ("member_id", "key", "other_member_id"),
342 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
343 "key" TEXT NOT NULL,
344 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
345 "value" TEXT NOT NULL );
347 COMMENT ON TABLE "member_relation_setting" IS 'Place to store a frontend specific setting related to relations between members as a string';
350 CREATE TYPE "member_image_type" AS ENUM ('photo', 'avatar');
352 COMMENT ON TYPE "member_image_type" IS 'Types of images for a member';
355 CREATE TABLE "member_image" (
356 PRIMARY KEY ("member_id", "image_type", "scaled"),
357 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
358 "image_type" "member_image_type",
359 "scaled" BOOLEAN,
360 "content_type" TEXT,
361 "data" BYTEA NOT NULL );
363 COMMENT ON TABLE "member_image" IS 'Images of members';
365 COMMENT ON COLUMN "member_image"."scaled" IS 'FALSE for original image, TRUE for scaled version of the image';
368 CREATE TABLE "member_count" (
369 "calculated" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
370 "total_count" INT4 NOT NULL );
372 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';
374 COMMENT ON COLUMN "member_count"."calculated" IS 'timestamp indicating when the total member count and area member counts were calculated';
375 COMMENT ON COLUMN "member_count"."total_count" IS 'Total count of active(!) members';
378 CREATE TABLE "contact" (
379 PRIMARY KEY ("member_id", "other_member_id"),
380 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
381 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
382 "public" BOOLEAN NOT NULL DEFAULT FALSE,
383 CONSTRAINT "cant_save_yourself_as_contact"
384 CHECK ("member_id" != "other_member_id") );
385 CREATE INDEX "contact_other_member_id_idx" ON "contact" ("other_member_id");
387 COMMENT ON TABLE "contact" IS 'Contact lists';
389 COMMENT ON COLUMN "contact"."member_id" IS 'Member having the contact list';
390 COMMENT ON COLUMN "contact"."other_member_id" IS 'Member referenced in the contact list';
391 COMMENT ON COLUMN "contact"."public" IS 'TRUE = display contact publically';
394 CREATE TABLE "ignored_member" (
395 PRIMARY KEY ("member_id", "other_member_id"),
396 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
397 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
398 CREATE INDEX "ignored_member_other_member_id_idx" ON "ignored_member" ("other_member_id");
400 COMMENT ON TABLE "ignored_member" IS 'Possibility to filter other members';
402 COMMENT ON COLUMN "ignored_member"."member_id" IS 'Member ignoring someone';
403 COMMENT ON COLUMN "ignored_member"."other_member_id" IS 'Member being ignored';
406 CREATE TABLE "session" (
407 "ident" TEXT PRIMARY KEY,
408 "additional_secret" TEXT,
409 "expiry" TIMESTAMPTZ NOT NULL DEFAULT now() + '24 hours',
410 "member_id" INT8 REFERENCES "member" ("id") ON DELETE SET NULL,
411 "lang" TEXT );
412 CREATE INDEX "session_expiry_idx" ON "session" ("expiry");
414 COMMENT ON TABLE "session" IS 'Sessions, i.e. for a web-frontend or API layer';
416 COMMENT ON COLUMN "session"."ident" IS 'Secret session identifier (i.e. random string)';
417 COMMENT ON COLUMN "session"."additional_secret" IS 'Additional field to store a secret, which can be used against CSRF attacks';
418 COMMENT ON COLUMN "session"."member_id" IS 'Reference to member, who is logged in';
419 COMMENT ON COLUMN "session"."lang" IS 'Language code of the selected language';
422 CREATE TABLE "policy" (
423 "id" SERIAL4 PRIMARY KEY,
424 "index" INT4 NOT NULL,
425 "active" BOOLEAN NOT NULL DEFAULT TRUE,
426 "name" TEXT NOT NULL UNIQUE,
427 "description" TEXT NOT NULL DEFAULT '',
428 "polling" BOOLEAN NOT NULL DEFAULT FALSE,
429 "admission_time" INTERVAL,
430 "discussion_time" INTERVAL,
431 "verification_time" INTERVAL,
432 "voting_time" INTERVAL,
433 "issue_quorum_num" INT4 NOT NULL,
434 "issue_quorum_den" INT4 NOT NULL,
435 "initiative_quorum_num" INT4 NOT NULL,
436 "initiative_quorum_den" INT4 NOT NULL,
437 "direct_majority_num" INT4 NOT NULL DEFAULT 1,
438 "direct_majority_den" INT4 NOT NULL DEFAULT 2,
439 "direct_majority_strict" BOOLEAN NOT NULL DEFAULT TRUE,
440 "direct_majority_positive" INT4 NOT NULL DEFAULT 0,
441 "direct_majority_non_negative" INT4 NOT NULL DEFAULT 0,
442 "indirect_majority_num" INT4 NOT NULL DEFAULT 1,
443 "indirect_majority_den" INT4 NOT NULL DEFAULT 2,
444 "indirect_majority_strict" BOOLEAN NOT NULL DEFAULT TRUE,
445 "indirect_majority_positive" INT4 NOT NULL DEFAULT 0,
446 "indirect_majority_non_negative" INT4 NOT NULL DEFAULT 0,
447 "no_reverse_beat_path" BOOLEAN NOT NULL DEFAULT TRUE,
448 "no_multistage_majority" BOOLEAN NOT NULL DEFAULT FALSE,
449 CONSTRAINT "timing" CHECK (
450 ( "polling" = FALSE AND
451 "admission_time" NOTNULL AND "discussion_time" NOTNULL AND
452 "verification_time" NOTNULL AND "voting_time" NOTNULL ) OR
453 ( "polling" = TRUE AND
454 "admission_time" ISNULL AND "discussion_time" NOTNULL AND
455 "verification_time" NOTNULL AND "voting_time" NOTNULL ) OR
456 ( "polling" = TRUE AND
457 "admission_time" ISNULL AND "discussion_time" ISNULL AND
458 "verification_time" ISNULL AND "voting_time" ISNULL ) ) );
459 CREATE INDEX "policy_active_idx" ON "policy" ("active");
461 COMMENT ON TABLE "policy" IS 'Policies for a particular proceeding type (timelimits, quorum)';
463 COMMENT ON COLUMN "policy"."index" IS 'Determines the order in listings';
464 COMMENT ON COLUMN "policy"."active" IS 'TRUE = policy can be used for new issues';
465 COMMENT ON COLUMN "policy"."polling" IS 'TRUE = special policy for non-user-generated issues, i.e. polls ("admission_time" MUST be set to NULL, the other timings may be set to NULL altogether, allowing individual timing for issues)';
466 COMMENT ON COLUMN "policy"."admission_time" IS 'Maximum duration of issue state ''admission''; Maximum time an issue stays open without being "accepted"';
467 COMMENT ON COLUMN "policy"."discussion_time" IS 'Duration of issue state ''discussion''; Regular time until an issue is "half_frozen" after being "accepted"';
468 COMMENT ON COLUMN "policy"."verification_time" IS 'Duration of issue state ''verification''; Regular time until an issue is "fully_frozen" (e.g. entering issue state ''voting'') after being "half_frozen"';
469 COMMENT ON COLUMN "policy"."voting_time" IS 'Duration of issue state ''voting''; Time after an issue is "fully_frozen" but not "closed" (duration of issue state ''voting'')';
470 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" and enter issue state ''discussion''';
471 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" and enter issue state ''discussion''';
472 COMMENT ON COLUMN "policy"."initiative_quorum_num" IS 'Numerator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
473 COMMENT ON COLUMN "policy"."initiative_quorum_den" IS 'Denominator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
474 COMMENT ON COLUMN "policy"."direct_majority_num" IS 'Numerator of fraction of neccessary direct majority for initiatives to be attainable as winner';
475 COMMENT ON COLUMN "policy"."direct_majority_den" IS 'Denominator of fraction of neccessary direct majority for initaitives to be attainable as winner';
476 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.';
477 COMMENT ON COLUMN "policy"."direct_majority_positive" IS 'Absolute number of "positive_votes" neccessary for an initiative to be attainable as winner';
478 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';
479 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';
480 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';
481 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.';
482 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';
483 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';
484 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.';
485 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").';
488 CREATE TABLE "unit" (
489 "id" SERIAL4 PRIMARY KEY,
490 "parent_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
491 "active" BOOLEAN NOT NULL DEFAULT TRUE,
492 "name" TEXT NOT NULL,
493 "description" TEXT NOT NULL DEFAULT '',
494 "member_count" INT4,
495 "text_search_data" TSVECTOR );
496 CREATE INDEX "unit_root_idx" ON "unit" ("id") WHERE "parent_id" ISNULL;
497 CREATE INDEX "unit_parent_id_idx" ON "unit" ("parent_id");
498 CREATE INDEX "unit_active_idx" ON "unit" ("active");
499 CREATE INDEX "unit_text_search_data_idx" ON "unit" USING gin ("text_search_data");
500 CREATE TRIGGER "update_text_search_data"
501 BEFORE INSERT OR UPDATE ON "unit"
502 FOR EACH ROW EXECUTE PROCEDURE
503 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
504 "name", "description" );
506 COMMENT ON TABLE "unit" IS 'Organizational units organized as trees; Delegations are not inherited through these trees.';
508 COMMENT ON COLUMN "unit"."parent_id" IS 'Parent id of tree node; Multiple roots allowed';
509 COMMENT ON COLUMN "unit"."active" IS 'TRUE means new issues can be created in areas of this unit';
510 COMMENT ON COLUMN "unit"."member_count" IS 'Count of members as determined by column "voting_right" in table "privilege"';
513 CREATE TABLE "unit_setting" (
514 PRIMARY KEY ("member_id", "key", "unit_id"),
515 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
516 "key" TEXT NOT NULL,
517 "unit_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
518 "value" TEXT NOT NULL );
520 COMMENT ON TABLE "unit_setting" IS 'Place for frontend to store unit specific settings of members as strings';
523 CREATE TABLE "area" (
524 "id" SERIAL4 PRIMARY KEY,
525 "unit_id" INT4 NOT NULL REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
526 "active" BOOLEAN NOT NULL DEFAULT TRUE,
527 "name" TEXT NOT NULL,
528 "description" TEXT NOT NULL DEFAULT '',
529 "direct_member_count" INT4,
530 "member_weight" INT4,
531 "text_search_data" TSVECTOR );
532 CREATE INDEX "area_unit_id_idx" ON "area" ("unit_id");
533 CREATE INDEX "area_active_idx" ON "area" ("active");
534 CREATE INDEX "area_text_search_data_idx" ON "area" USING gin ("text_search_data");
535 CREATE TRIGGER "update_text_search_data"
536 BEFORE INSERT OR UPDATE ON "area"
537 FOR EACH ROW EXECUTE PROCEDURE
538 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
539 "name", "description" );
541 COMMENT ON TABLE "area" IS 'Subject areas';
543 COMMENT ON COLUMN "area"."active" IS 'TRUE means new issues can be created in this area';
544 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"';
545 COMMENT ON COLUMN "area"."member_weight" IS 'Same as "direct_member_count" but respecting delegations';
548 CREATE TABLE "area_setting" (
549 PRIMARY KEY ("member_id", "key", "area_id"),
550 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
551 "key" TEXT NOT NULL,
552 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
553 "value" TEXT NOT NULL );
555 COMMENT ON TABLE "area_setting" IS 'Place for frontend to store area specific settings of members as strings';
558 CREATE TABLE "allowed_policy" (
559 PRIMARY KEY ("area_id", "policy_id"),
560 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
561 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
562 "default_policy" BOOLEAN NOT NULL DEFAULT FALSE );
563 CREATE UNIQUE INDEX "allowed_policy_one_default_per_area_idx" ON "allowed_policy" ("area_id") WHERE "default_policy";
565 COMMENT ON TABLE "allowed_policy" IS 'Selects which policies can be used in each area';
567 COMMENT ON COLUMN "allowed_policy"."default_policy" IS 'One policy per area can be set as default.';
570 CREATE TYPE "snapshot_event" AS ENUM ('periodic', 'end_of_admission', 'half_freeze', 'full_freeze');
572 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';
575 CREATE TYPE "issue_state" AS ENUM (
576 'admission', 'discussion', 'verification', 'voting',
577 'canceled_revoked_before_accepted',
578 'canceled_issue_not_accepted',
579 'canceled_after_revocation_during_discussion',
580 'canceled_after_revocation_during_verification',
581 'calculation',
582 'canceled_no_initiative_admitted',
583 'finished_without_winner', 'finished_with_winner');
585 COMMENT ON TYPE "issue_state" IS 'State of issues';
588 CREATE TABLE "issue" (
589 "id" SERIAL4 PRIMARY KEY,
590 "area_id" INT4 NOT NULL REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
591 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
592 "state" "issue_state" NOT NULL DEFAULT 'admission',
593 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
594 "accepted" TIMESTAMPTZ,
595 "half_frozen" TIMESTAMPTZ,
596 "fully_frozen" TIMESTAMPTZ,
597 "closed" TIMESTAMPTZ,
598 "ranks_available" BOOLEAN NOT NULL DEFAULT FALSE,
599 "cleaned" TIMESTAMPTZ,
600 "admission_time" INTERVAL NOT NULL,
601 "discussion_time" INTERVAL NOT NULL,
602 "verification_time" INTERVAL NOT NULL,
603 "voting_time" INTERVAL NOT NULL,
604 "snapshot" TIMESTAMPTZ,
605 "latest_snapshot_event" "snapshot_event",
606 "population" INT4,
607 "voter_count" INT4,
608 "status_quo_schulze_rank" INT4,
609 CONSTRAINT "valid_state" CHECK ((
610 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
611 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
612 ("accepted" NOTNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
613 ("accepted" NOTNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
614 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
615 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
616 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
617 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
618 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = TRUE)) AND (
619 ("state" = 'admission' AND "closed" ISNULL AND "accepted" ISNULL) OR
620 ("state" = 'discussion' AND "closed" ISNULL AND "accepted" NOTNULL AND "half_frozen" ISNULL) OR
621 ("state" = 'verification' AND "closed" ISNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL) OR
622 ("state" = 'voting' AND "closed" ISNULL AND "fully_frozen" NOTNULL) OR
623 ("state" = 'canceled_revoked_before_accepted' AND "closed" NOTNULL AND "accepted" ISNULL) OR
624 ("state" = 'canceled_issue_not_accepted' AND "closed" NOTNULL AND "accepted" ISNULL) OR
625 ("state" = 'canceled_after_revocation_during_discussion' AND "closed" NOTNULL AND "half_frozen" ISNULL) OR
626 ("state" = 'canceled_after_revocation_during_verification' AND "closed" NOTNULL AND "fully_frozen" ISNULL) OR
627 ("state" = 'calculation' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = FALSE) OR
628 ("state" = 'canceled_no_initiative_admitted' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = TRUE) OR
629 ("state" = 'finished_without_winner' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = TRUE) OR
630 ("state" = 'finished_with_winner' AND "closed" NOTNULL AND "fully_frozen" NOTNULL AND "ranks_available" = TRUE)
631 )),
632 CONSTRAINT "state_change_order" CHECK (
633 "created" <= "accepted" AND
634 "accepted" <= "half_frozen" AND
635 "half_frozen" <= "fully_frozen" AND
636 "fully_frozen" <= "closed" ),
637 CONSTRAINT "only_closed_issues_may_be_cleaned" CHECK (
638 "cleaned" ISNULL OR "closed" NOTNULL ),
639 CONSTRAINT "last_snapshot_on_full_freeze"
640 CHECK ("snapshot" = "fully_frozen"), -- NOTE: snapshot can be set, while frozen is NULL yet
641 CONSTRAINT "freeze_requires_snapshot"
642 CHECK ("fully_frozen" ISNULL OR "snapshot" NOTNULL),
643 CONSTRAINT "set_both_or_none_of_snapshot_and_latest_snapshot_event"
644 CHECK ("snapshot" NOTNULL = "latest_snapshot_event" NOTNULL) );
645 CREATE INDEX "issue_area_id_idx" ON "issue" ("area_id");
646 CREATE INDEX "issue_policy_id_idx" ON "issue" ("policy_id");
647 CREATE INDEX "issue_created_idx" ON "issue" ("created");
648 CREATE INDEX "issue_accepted_idx" ON "issue" ("accepted");
649 CREATE INDEX "issue_half_frozen_idx" ON "issue" ("half_frozen");
650 CREATE INDEX "issue_fully_frozen_idx" ON "issue" ("fully_frozen");
651 CREATE INDEX "issue_closed_idx" ON "issue" ("closed");
652 CREATE INDEX "issue_created_idx_open" ON "issue" ("created") WHERE "closed" ISNULL;
653 CREATE INDEX "issue_closed_idx_canceled" ON "issue" ("closed") WHERE "fully_frozen" ISNULL;
655 COMMENT ON TABLE "issue" IS 'Groups of initiatives';
657 COMMENT ON COLUMN "issue"."accepted" IS 'Point in time, when one initiative of issue reached the "issue_quorum"';
658 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.';
659 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.';
660 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.';
661 COMMENT ON COLUMN "issue"."ranks_available" IS 'TRUE = ranks have been calculated';
662 COMMENT ON COLUMN "issue"."cleaned" IS 'Point in time, when discussion data and votes had been deleted';
663 COMMENT ON COLUMN "issue"."admission_time" IS 'Copied from "policy" table at creation of issue';
664 COMMENT ON COLUMN "issue"."discussion_time" IS 'Copied from "policy" table at creation of issue';
665 COMMENT ON COLUMN "issue"."verification_time" IS 'Copied from "policy" table at creation of issue';
666 COMMENT ON COLUMN "issue"."voting_time" IS 'Copied from "policy" table at creation of issue';
667 COMMENT ON COLUMN "issue"."snapshot" IS 'Point in time, when snapshot tables have been updated and "population" and *_count values were precalculated';
668 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';
669 COMMENT ON COLUMN "issue"."population" IS 'Sum of "weight" column in table "direct_population_snapshot"';
670 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';
671 COMMENT ON COLUMN "issue"."status_quo_schulze_rank" IS 'Schulze rank of status quo, as calculated by "calculate_ranks" function';
674 CREATE TABLE "issue_setting" (
675 PRIMARY KEY ("member_id", "key", "issue_id"),
676 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
677 "key" TEXT NOT NULL,
678 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
679 "value" TEXT NOT NULL );
681 COMMENT ON TABLE "issue_setting" IS 'Place for frontend to store issue specific settings of members as strings';
684 CREATE TABLE "initiative" (
685 UNIQUE ("issue_id", "id"), -- index needed for foreign-key on table "vote"
686 "issue_id" INT4 NOT NULL REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
687 "id" SERIAL4 PRIMARY KEY,
688 "name" TEXT NOT NULL,
689 "polling" BOOLEAN NOT NULL DEFAULT FALSE,
690 "discussion_url" TEXT,
691 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
692 "revoked" TIMESTAMPTZ,
693 "revoked_by_member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
694 "suggested_initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
695 "admitted" BOOLEAN,
696 "supporter_count" INT4,
697 "informed_supporter_count" INT4,
698 "satisfied_supporter_count" INT4,
699 "satisfied_informed_supporter_count" INT4,
700 "positive_votes" INT4,
701 "negative_votes" INT4,
702 "direct_majority" BOOLEAN,
703 "indirect_majority" BOOLEAN,
704 "schulze_rank" INT4,
705 "better_than_status_quo" BOOLEAN,
706 "worse_than_status_quo" BOOLEAN,
707 "reverse_beat_path" BOOLEAN,
708 "multistage_majority" BOOLEAN,
709 "eligible" BOOLEAN,
710 "winner" BOOLEAN,
711 "rank" INT4,
712 "text_search_data" TSVECTOR,
713 CONSTRAINT "all_or_none_of_revoked_and_revoked_by_member_id_must_be_null"
714 CHECK ("revoked" NOTNULL = "revoked_by_member_id" NOTNULL),
715 CONSTRAINT "non_revoked_initiatives_cant_suggest_other"
716 CHECK ("revoked" NOTNULL OR "suggested_initiative_id" ISNULL),
717 CONSTRAINT "revoked_initiatives_cant_be_admitted"
718 CHECK ("revoked" ISNULL OR "admitted" ISNULL),
719 CONSTRAINT "non_admitted_initiatives_cant_contain_voting_results" CHECK (
720 ( "admitted" NOTNULL AND "admitted" = TRUE ) OR
721 ( "positive_votes" ISNULL AND "negative_votes" ISNULL AND
722 "direct_majority" ISNULL AND "indirect_majority" ISNULL AND
723 "schulze_rank" ISNULL AND
724 "better_than_status_quo" ISNULL AND "worse_than_status_quo" ISNULL AND
725 "reverse_beat_path" ISNULL AND "multistage_majority" ISNULL AND
726 "eligible" ISNULL AND "winner" ISNULL AND "rank" ISNULL ) ),
727 CONSTRAINT "better_excludes_worse" CHECK (NOT ("better_than_status_quo" AND "worse_than_status_quo")),
728 CONSTRAINT "minimum_requirement_to_be_eligible" CHECK (
729 "eligible" = FALSE OR
730 ("direct_majority" AND "indirect_majority" AND "better_than_status_quo") ),
731 CONSTRAINT "winner_must_be_eligible" CHECK ("winner"=FALSE OR "eligible"=TRUE),
732 CONSTRAINT "winner_must_have_first_rank" CHECK ("winner"=FALSE OR "rank"=1),
733 CONSTRAINT "eligible_at_first_rank_is_winner" CHECK ("eligible"=FALSE OR "rank"!=1 OR "winner"=TRUE),
734 CONSTRAINT "unique_rank_per_issue" UNIQUE ("issue_id", "rank") );
735 CREATE INDEX "initiative_created_idx" ON "initiative" ("created");
736 CREATE INDEX "initiative_revoked_idx" ON "initiative" ("revoked");
737 CREATE INDEX "initiative_text_search_data_idx" ON "initiative" USING gin ("text_search_data");
738 CREATE TRIGGER "update_text_search_data"
739 BEFORE INSERT OR UPDATE ON "initiative"
740 FOR EACH ROW EXECUTE PROCEDURE
741 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
742 "name", "discussion_url");
744 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.';
746 COMMENT ON COLUMN "initiative"."polling" IS 'Initiative is an option for a poll (see "policy"."polling"), and does not need to pass the initiative quorum';
747 COMMENT ON COLUMN "initiative"."discussion_url" IS 'URL pointing to a discussion platform for this initiative';
748 COMMENT ON COLUMN "initiative"."revoked" IS 'Point in time, when one initiator decided to revoke the initiative';
749 COMMENT ON COLUMN "initiative"."revoked_by_member_id" IS 'Member, who decided to revoke the initiative';
750 COMMENT ON COLUMN "initiative"."admitted" IS 'TRUE, if initiative reaches the "initiative_quorum" when freezing the issue';
751 COMMENT ON COLUMN "initiative"."supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
752 COMMENT ON COLUMN "initiative"."informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
753 COMMENT ON COLUMN "initiative"."satisfied_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
754 COMMENT ON COLUMN "initiative"."satisfied_informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
755 COMMENT ON COLUMN "initiative"."positive_votes" IS 'Calculated from table "direct_voter"';
756 COMMENT ON COLUMN "initiative"."negative_votes" IS 'Calculated from table "direct_voter"';
757 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"';
758 COMMENT ON COLUMN "initiative"."indirect_majority" IS 'Same as "direct_majority", but also considering indirect beat paths';
759 COMMENT ON COLUMN "initiative"."schulze_rank" IS 'Schulze-Ranking without tie-breaking';
760 COMMENT ON COLUMN "initiative"."better_than_status_quo" IS 'TRUE, if initiative has a schulze-ranking better than the status quo (without tie-breaking)';
761 COMMENT ON COLUMN "initiative"."worse_than_status_quo" IS 'TRUE, if initiative has a schulze-ranking worse than the status quo (without tie-breaking)';
762 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';
763 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';
764 COMMENT ON COLUMN "initiative"."eligible" IS 'Initiative has a "direct_majority" and an "indirect_majority", is "better_than_status_quo" and depending on selected policy the initiative has no "reverse_beat_path" or "multistage_majority"';
765 COMMENT ON COLUMN "initiative"."winner" IS 'Winner is the "eligible" initiative with best "schulze_rank" and in case of ties with lowest "id"';
766 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';
769 CREATE TABLE "battle" (
770 "issue_id" INT4 NOT NULL,
771 "winning_initiative_id" INT4,
772 FOREIGN KEY ("issue_id", "winning_initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
773 "losing_initiative_id" INT4,
774 FOREIGN KEY ("issue_id", "losing_initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
775 "count" INT4 NOT NULL,
776 CONSTRAINT "initiative_ids_not_equal" CHECK (
777 "winning_initiative_id" != "losing_initiative_id" OR
778 ( ("winning_initiative_id" NOTNULL AND "losing_initiative_id" ISNULL) OR
779 ("winning_initiative_id" ISNULL AND "losing_initiative_id" NOTNULL) ) ) );
780 CREATE UNIQUE INDEX "battle_winning_losing_idx" ON "battle" ("issue_id", "winning_initiative_id", "losing_initiative_id");
781 CREATE UNIQUE INDEX "battle_winning_null_idx" ON "battle" ("issue_id", "winning_initiative_id") WHERE "losing_initiative_id" ISNULL;
782 CREATE UNIQUE INDEX "battle_null_losing_idx" ON "battle" ("issue_id", "losing_initiative_id") WHERE "winning_initiative_id" ISNULL;
784 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';
787 CREATE TABLE "ignored_initiative" (
788 PRIMARY KEY ("initiative_id", "member_id"),
789 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
790 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
791 CREATE INDEX "ignored_initiative_member_id_idx" ON "ignored_initiative" ("member_id");
793 COMMENT ON TABLE "ignored_initiative" IS 'Possibility to filter initiatives';
796 CREATE TABLE "initiative_setting" (
797 PRIMARY KEY ("member_id", "key", "initiative_id"),
798 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
799 "key" TEXT NOT NULL,
800 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
801 "value" TEXT NOT NULL );
803 COMMENT ON TABLE "initiative_setting" IS 'Place for frontend to store initiative specific settings of members as strings';
806 CREATE TABLE "draft" (
807 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "supporter"
808 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
809 "id" SERIAL8 PRIMARY KEY,
810 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
811 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
812 "formatting_engine" TEXT,
813 "content" TEXT NOT NULL,
814 "text_search_data" TSVECTOR );
815 CREATE INDEX "draft_created_idx" ON "draft" ("created");
816 CREATE INDEX "draft_author_id_created_idx" ON "draft" ("author_id", "created");
817 CREATE INDEX "draft_text_search_data_idx" ON "draft" USING gin ("text_search_data");
818 CREATE TRIGGER "update_text_search_data"
819 BEFORE INSERT OR UPDATE ON "draft"
820 FOR EACH ROW EXECUTE PROCEDURE
821 tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "content");
823 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.';
825 COMMENT ON COLUMN "draft"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used';
826 COMMENT ON COLUMN "draft"."content" IS 'Text of the draft in a format depending on the field "formatting_engine"';
829 CREATE TABLE "rendered_draft" (
830 PRIMARY KEY ("draft_id", "format"),
831 "draft_id" INT8 REFERENCES "draft" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
832 "format" TEXT,
833 "content" TEXT NOT NULL );
835 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)';
838 CREATE TABLE "suggestion" (
839 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "opinion"
840 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
841 "id" SERIAL8 PRIMARY KEY,
842 "draft_id" INT8 NOT NULL,
843 FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE NO ACTION ON UPDATE CASCADE,
844 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
845 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
846 "name" TEXT NOT NULL,
847 "formatting_engine" TEXT,
848 "content" TEXT NOT NULL DEFAULT '',
849 "text_search_data" TSVECTOR,
850 "minus2_unfulfilled_count" INT4,
851 "minus2_fulfilled_count" INT4,
852 "minus1_unfulfilled_count" INT4,
853 "minus1_fulfilled_count" INT4,
854 "plus1_unfulfilled_count" INT4,
855 "plus1_fulfilled_count" INT4,
856 "plus2_unfulfilled_count" INT4,
857 "plus2_fulfilled_count" INT4 );
858 CREATE INDEX "suggestion_created_idx" ON "suggestion" ("created");
859 CREATE INDEX "suggestion_author_id_created_idx" ON "suggestion" ("author_id", "created");
860 CREATE INDEX "suggestion_text_search_data_idx" ON "suggestion" USING gin ("text_search_data");
861 CREATE TRIGGER "update_text_search_data"
862 BEFORE INSERT OR UPDATE ON "suggestion"
863 FOR EACH ROW EXECUTE PROCEDURE
864 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
865 "name", "content");
867 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';
869 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")';
870 COMMENT ON COLUMN "suggestion"."minus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
871 COMMENT ON COLUMN "suggestion"."minus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
872 COMMENT ON COLUMN "suggestion"."minus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
873 COMMENT ON COLUMN "suggestion"."minus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
874 COMMENT ON COLUMN "suggestion"."plus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
875 COMMENT ON COLUMN "suggestion"."plus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
876 COMMENT ON COLUMN "suggestion"."plus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
877 COMMENT ON COLUMN "suggestion"."plus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
880 CREATE TABLE "rendered_suggestion" (
881 PRIMARY KEY ("suggestion_id", "format"),
882 "suggestion_id" INT8 REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
883 "format" TEXT,
884 "content" TEXT NOT NULL );
886 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)';
889 CREATE TABLE "suggestion_setting" (
890 PRIMARY KEY ("member_id", "key", "suggestion_id"),
891 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
892 "key" TEXT NOT NULL,
893 "suggestion_id" INT8 REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
894 "value" TEXT NOT NULL );
896 COMMENT ON TABLE "suggestion_setting" IS 'Place for frontend to store suggestion specific settings of members as strings';
899 CREATE TABLE "privilege" (
900 PRIMARY KEY ("unit_id", "member_id"),
901 "unit_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
902 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
903 "admin_manager" BOOLEAN NOT NULL DEFAULT FALSE,
904 "unit_manager" BOOLEAN NOT NULL DEFAULT FALSE,
905 "area_manager" BOOLEAN NOT NULL DEFAULT FALSE,
906 "member_manager" BOOLEAN NOT NULL DEFAULT FALSE,
907 "initiative_right" BOOLEAN NOT NULL DEFAULT TRUE,
908 "voting_right" BOOLEAN NOT NULL DEFAULT TRUE,
909 "polling_right" BOOLEAN NOT NULL DEFAULT FALSE );
911 COMMENT ON TABLE "privilege" IS 'Members rights related to each unit';
913 COMMENT ON COLUMN "privilege"."admin_manager" IS 'Grant/revoke any privileges to/from other members';
914 COMMENT ON COLUMN "privilege"."unit_manager" IS 'Create and disable sub units';
915 COMMENT ON COLUMN "privilege"."area_manager" IS 'Create and disable areas and set area parameters';
916 COMMENT ON COLUMN "privilege"."member_manager" IS 'Adding/removing members from the unit, granting or revoking "initiative_right" and "voting_right"';
917 COMMENT ON COLUMN "privilege"."initiative_right" IS 'Right to create an initiative';
918 COMMENT ON COLUMN "privilege"."voting_right" IS 'Right to support initiatives, create and rate suggestions, and to vote';
919 COMMENT ON COLUMN "privilege"."polling_right" IS 'Right to create polls (see "policy"."polling" and "initiative"."polling")';
922 CREATE TABLE "membership" (
923 PRIMARY KEY ("area_id", "member_id"),
924 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
925 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
926 CREATE INDEX "membership_member_id_idx" ON "membership" ("member_id");
928 COMMENT ON TABLE "membership" IS 'Interest of members in topic areas';
931 CREATE TABLE "interest" (
932 PRIMARY KEY ("issue_id", "member_id"),
933 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
934 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
935 CREATE INDEX "interest_member_id_idx" ON "interest" ("member_id");
937 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.';
940 CREATE TABLE "initiator" (
941 PRIMARY KEY ("initiative_id", "member_id"),
942 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
943 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
944 "accepted" BOOLEAN );
945 CREATE INDEX "initiator_member_id_idx" ON "initiator" ("member_id");
947 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.';
949 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.';
952 CREATE TABLE "supporter" (
953 "issue_id" INT4 NOT NULL,
954 PRIMARY KEY ("initiative_id", "member_id"),
955 "initiative_id" INT4,
956 "member_id" INT4,
957 "draft_id" INT8 NOT NULL,
958 FOREIGN KEY ("issue_id", "member_id") REFERENCES "interest" ("issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE,
959 FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE NO ACTION ON UPDATE CASCADE );
960 CREATE INDEX "supporter_member_id_idx" ON "supporter" ("member_id");
962 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.';
964 COMMENT ON COLUMN "supporter"."issue_id" IS 'WARNING: No index: For selections use column "initiative_id" and join via table "initiative" where neccessary';
965 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")';
968 CREATE TABLE "opinion" (
969 "initiative_id" INT4 NOT NULL,
970 PRIMARY KEY ("suggestion_id", "member_id"),
971 "suggestion_id" INT8,
972 "member_id" INT4,
973 "degree" INT2 NOT NULL CHECK ("degree" >= -2 AND "degree" <= 2 AND "degree" != 0),
974 "fulfilled" BOOLEAN NOT NULL DEFAULT FALSE,
975 FOREIGN KEY ("initiative_id", "suggestion_id") REFERENCES "suggestion" ("initiative_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
976 FOREIGN KEY ("initiative_id", "member_id") REFERENCES "supporter" ("initiative_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
977 CREATE INDEX "opinion_member_id_initiative_id_idx" ON "opinion" ("member_id", "initiative_id");
979 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.';
981 COMMENT ON COLUMN "opinion"."degree" IS '2 = fulfillment required for support; 1 = fulfillment desired; -1 = fulfillment unwanted; -2 = fulfillment cancels support';
984 CREATE TYPE "delegation_scope" AS ENUM ('unit', 'area', 'issue');
986 COMMENT ON TYPE "delegation_scope" IS 'Scope for delegations: ''unit'', ''area'', or ''issue'' (order is relevant)';
989 CREATE TABLE "delegation" (
990 "id" SERIAL8 PRIMARY KEY,
991 "truster_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
992 "trustee_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
993 "scope" "delegation_scope" NOT NULL,
994 "unit_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
995 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
996 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
997 CONSTRAINT "cant_delegate_to_yourself" CHECK ("truster_id" != "trustee_id"),
998 CONSTRAINT "no_unit_delegation_to_null"
999 CHECK ("trustee_id" NOTNULL OR "scope" != 'unit'),
1000 CONSTRAINT "area_id_and_issue_id_set_according_to_scope" CHECK (
1001 ("scope" = 'unit' AND "unit_id" NOTNULL AND "area_id" ISNULL AND "issue_id" ISNULL ) OR
1002 ("scope" = 'area' AND "unit_id" ISNULL AND "area_id" NOTNULL AND "issue_id" ISNULL ) OR
1003 ("scope" = 'issue' AND "unit_id" ISNULL AND "area_id" ISNULL AND "issue_id" NOTNULL) ),
1004 UNIQUE ("unit_id", "truster_id"),
1005 UNIQUE ("area_id", "truster_id"),
1006 UNIQUE ("issue_id", "truster_id") );
1007 CREATE INDEX "delegation_truster_id_idx" ON "delegation" ("truster_id");
1008 CREATE INDEX "delegation_trustee_id_idx" ON "delegation" ("trustee_id");
1010 COMMENT ON TABLE "delegation" IS 'Delegation of vote-weight to other members';
1012 COMMENT ON COLUMN "delegation"."unit_id" IS 'Reference to unit, if delegation is unit-wide, otherwise NULL';
1013 COMMENT ON COLUMN "delegation"."area_id" IS 'Reference to area, if delegation is area-wide, otherwise NULL';
1014 COMMENT ON COLUMN "delegation"."issue_id" IS 'Reference to issue, if delegation is issue-wide, otherwise NULL';
1017 CREATE TABLE "direct_population_snapshot" (
1018 PRIMARY KEY ("issue_id", "event", "member_id"),
1019 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1020 "event" "snapshot_event",
1021 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
1022 "weight" INT4 );
1023 CREATE INDEX "direct_population_snapshot_member_id_idx" ON "direct_population_snapshot" ("member_id");
1025 COMMENT ON TABLE "direct_population_snapshot" IS 'Snapshot of active members having either a "membership" in the "area" or an "interest" in the "issue"';
1027 COMMENT ON COLUMN "direct_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
1028 COMMENT ON COLUMN "direct_population_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_population_snapshot"';
1031 CREATE TABLE "delegating_population_snapshot" (
1032 PRIMARY KEY ("issue_id", "event", "member_id"),
1033 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1034 "event" "snapshot_event",
1035 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
1036 "weight" INT4,
1037 "scope" "delegation_scope" NOT NULL,
1038 "delegate_member_ids" INT4[] NOT NULL );
1039 CREATE INDEX "delegating_population_snapshot_member_id_idx" ON "delegating_population_snapshot" ("member_id");
1041 COMMENT ON TABLE "direct_population_snapshot" IS 'Delegations increasing the weight of entries in the "direct_population_snapshot" table';
1043 COMMENT ON COLUMN "delegating_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
1044 COMMENT ON COLUMN "delegating_population_snapshot"."member_id" IS 'Delegating member';
1045 COMMENT ON COLUMN "delegating_population_snapshot"."weight" IS 'Intermediate weight';
1046 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"';
1049 CREATE TABLE "direct_interest_snapshot" (
1050 PRIMARY KEY ("issue_id", "event", "member_id"),
1051 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1052 "event" "snapshot_event",
1053 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
1054 "weight" INT4 );
1055 CREATE INDEX "direct_interest_snapshot_member_id_idx" ON "direct_interest_snapshot" ("member_id");
1057 COMMENT ON TABLE "direct_interest_snapshot" IS 'Snapshot of active members having an "interest" in the "issue"';
1059 COMMENT ON COLUMN "direct_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
1060 COMMENT ON COLUMN "direct_interest_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_interest_snapshot"';
1063 CREATE TABLE "delegating_interest_snapshot" (
1064 PRIMARY KEY ("issue_id", "event", "member_id"),
1065 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1066 "event" "snapshot_event",
1067 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
1068 "weight" INT4,
1069 "scope" "delegation_scope" NOT NULL,
1070 "delegate_member_ids" INT4[] NOT NULL );
1071 CREATE INDEX "delegating_interest_snapshot_member_id_idx" ON "delegating_interest_snapshot" ("member_id");
1073 COMMENT ON TABLE "delegating_interest_snapshot" IS 'Delegations increasing the weight of entries in the "direct_interest_snapshot" table';
1075 COMMENT ON COLUMN "delegating_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
1076 COMMENT ON COLUMN "delegating_interest_snapshot"."member_id" IS 'Delegating member';
1077 COMMENT ON COLUMN "delegating_interest_snapshot"."weight" IS 'Intermediate weight';
1078 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"';
1081 CREATE TABLE "direct_supporter_snapshot" (
1082 "issue_id" INT4 NOT NULL,
1083 PRIMARY KEY ("initiative_id", "event", "member_id"),
1084 "initiative_id" INT4,
1085 "event" "snapshot_event",
1086 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
1087 "draft_id" INT8 NOT NULL,
1088 "informed" BOOLEAN NOT NULL,
1089 "satisfied" BOOLEAN NOT NULL,
1090 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
1091 FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE NO ACTION ON UPDATE CASCADE,
1092 FOREIGN KEY ("issue_id", "event", "member_id") REFERENCES "direct_interest_snapshot" ("issue_id", "event", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
1093 CREATE INDEX "direct_supporter_snapshot_member_id_idx" ON "direct_supporter_snapshot" ("member_id");
1095 COMMENT ON TABLE "direct_supporter_snapshot" IS 'Snapshot of supporters of initiatives (weight is stored in "direct_interest_snapshot")';
1097 COMMENT ON COLUMN "direct_supporter_snapshot"."issue_id" IS 'WARNING: No index: For selections use column "initiative_id" and join via table "initiative" where neccessary';
1098 COMMENT ON COLUMN "direct_supporter_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
1099 COMMENT ON COLUMN "direct_supporter_snapshot"."informed" IS 'Supporter has seen the latest draft of the initiative';
1100 COMMENT ON COLUMN "direct_supporter_snapshot"."satisfied" IS 'Supporter has no "critical_opinion"s';
1103 CREATE TABLE "non_voter" (
1104 PRIMARY KEY ("issue_id", "member_id"),
1105 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1106 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
1107 CREATE INDEX "non_voter_member_id_idx" ON "non_voter" ("member_id");
1109 COMMENT ON TABLE "non_voter" IS 'Members who decided to not vote directly on an issue';
1112 CREATE TABLE "direct_voter" (
1113 PRIMARY KEY ("issue_id", "member_id"),
1114 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1115 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
1116 "weight" INT4 );
1117 CREATE INDEX "direct_voter_member_id_idx" ON "direct_voter" ("member_id");
1119 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.';
1121 COMMENT ON COLUMN "direct_voter"."weight" IS 'Weight of member (1 or higher) according to "delegating_voter" table';
1124 CREATE TABLE "delegating_voter" (
1125 PRIMARY KEY ("issue_id", "member_id"),
1126 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1127 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
1128 "weight" INT4,
1129 "scope" "delegation_scope" NOT NULL,
1130 "delegate_member_ids" INT4[] NOT NULL );
1131 CREATE INDEX "delegating_voter_member_id_idx" ON "delegating_voter" ("member_id");
1133 COMMENT ON TABLE "delegating_voter" IS 'Delegations increasing the weight of entries in the "direct_voter" table';
1135 COMMENT ON COLUMN "delegating_voter"."member_id" IS 'Delegating member';
1136 COMMENT ON COLUMN "delegating_voter"."weight" IS 'Intermediate weight';
1137 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"';
1140 CREATE TABLE "vote" (
1141 "issue_id" INT4 NOT NULL,
1142 PRIMARY KEY ("initiative_id", "member_id"),
1143 "initiative_id" INT4,
1144 "member_id" INT4,
1145 "grade" INT4,
1146 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
1147 FOREIGN KEY ("issue_id", "member_id") REFERENCES "direct_voter" ("issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
1148 CREATE INDEX "vote_member_id_idx" ON "vote" ("member_id");
1150 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.';
1152 COMMENT ON COLUMN "vote"."issue_id" IS 'WARNING: No index: For selections use column "initiative_id" and join via table "initiative" where neccessary';
1153 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.';
1156 CREATE TABLE "voting_comment" (
1157 PRIMARY KEY ("issue_id", "member_id"),
1158 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1159 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1160 "changed" TIMESTAMPTZ,
1161 "formatting_engine" TEXT,
1162 "content" TEXT NOT NULL,
1163 "text_search_data" TSVECTOR );
1164 CREATE INDEX "voting_comment_member_id_idx" ON "voting_comment" ("member_id");
1165 CREATE INDEX "voting_comment_text_search_data_idx" ON "voting_comment" USING gin ("text_search_data");
1166 CREATE TRIGGER "update_text_search_data"
1167 BEFORE INSERT OR UPDATE ON "voting_comment"
1168 FOR EACH ROW EXECUTE PROCEDURE
1169 tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "content");
1171 COMMENT ON TABLE "voting_comment" IS 'Storage for comments of voters to be published after voting has finished.';
1173 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.';
1176 CREATE TABLE "rendered_voting_comment" (
1177 PRIMARY KEY ("issue_id", "member_id", "format"),
1178 FOREIGN KEY ("issue_id", "member_id")
1179 REFERENCES "voting_comment" ("issue_id", "member_id")
1180 ON DELETE CASCADE ON UPDATE CASCADE,
1181 "issue_id" INT4,
1182 "member_id" INT4,
1183 "format" TEXT,
1184 "content" TEXT NOT NULL );
1186 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)';
1189 CREATE TYPE "event_type" AS ENUM (
1190 'issue_state_changed',
1191 'initiative_created_in_new_issue',
1192 'initiative_created_in_existing_issue',
1193 'initiative_revoked',
1194 'new_draft_created',
1195 'suggestion_created');
1197 COMMENT ON TYPE "event_type" IS 'Type used for column "event" of table "event"';
1200 CREATE TABLE "event" (
1201 "id" SERIAL8 PRIMARY KEY,
1202 "occurrence" TIMESTAMPTZ NOT NULL DEFAULT now(),
1203 "event" "event_type" NOT NULL,
1204 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
1205 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
1206 "state" "issue_state" CHECK ("state" != 'calculation'),
1207 "initiative_id" INT4,
1208 "draft_id" INT8,
1209 "suggestion_id" INT8,
1210 FOREIGN KEY ("issue_id", "initiative_id")
1211 REFERENCES "initiative" ("issue_id", "id")
1212 ON DELETE CASCADE ON UPDATE CASCADE,
1213 FOREIGN KEY ("initiative_id", "draft_id")
1214 REFERENCES "draft" ("initiative_id", "id")
1215 ON DELETE CASCADE ON UPDATE CASCADE,
1216 FOREIGN KEY ("initiative_id", "suggestion_id")
1217 REFERENCES "suggestion" ("initiative_id", "id")
1218 ON DELETE CASCADE ON UPDATE CASCADE,
1219 CONSTRAINT "null_constraints_for_issue_state_changed" CHECK (
1220 "event" != 'issue_state_changed' OR (
1221 "member_id" ISNULL AND
1222 "issue_id" NOTNULL AND
1223 "state" NOTNULL AND
1224 "initiative_id" ISNULL AND
1225 "draft_id" ISNULL AND
1226 "suggestion_id" ISNULL )),
1227 CONSTRAINT "null_constraints_for_initiative_creation_or_revocation_or_new_draft" CHECK (
1228 "event" NOT IN (
1229 'initiative_created_in_new_issue',
1230 'initiative_created_in_existing_issue',
1231 'initiative_revoked',
1232 'new_draft_created'
1233 ) OR (
1234 "member_id" NOTNULL AND
1235 "issue_id" NOTNULL AND
1236 "state" NOTNULL AND
1237 "initiative_id" NOTNULL AND
1238 "draft_id" NOTNULL AND
1239 "suggestion_id" ISNULL )),
1240 CONSTRAINT "null_constraints_for_suggestion_creation" CHECK (
1241 "event" != 'suggestion_created' OR (
1242 "member_id" NOTNULL AND
1243 "issue_id" NOTNULL AND
1244 "state" NOTNULL AND
1245 "initiative_id" NOTNULL AND
1246 "draft_id" ISNULL AND
1247 "suggestion_id" NOTNULL )) );
1248 CREATE INDEX "event_occurrence_idx" ON "event" ("occurrence");
1250 COMMENT ON TABLE "event" IS 'Event table, automatically filled by triggers';
1252 COMMENT ON COLUMN "event"."occurrence" IS 'Point in time, when event occurred';
1253 COMMENT ON COLUMN "event"."event" IS 'Type of event (see TYPE "event_type")';
1254 COMMENT ON COLUMN "event"."member_id" IS 'Member who caused the event, if applicable';
1255 COMMENT ON COLUMN "event"."state" IS 'If issue_id is set: state of affected issue; If state changed: new state';
1258 CREATE TABLE "notification_sent" (
1259 "event_id" INT8 NOT NULL );
1260 CREATE UNIQUE INDEX "notification_sent_singleton_idx" ON "notification_sent" ((1));
1262 COMMENT ON TABLE "notification_sent" IS 'This table stores one row with the last event_id, for which notifications have been sent out';
1263 COMMENT ON INDEX "notification_sent_singleton_idx" IS 'This index ensures that "notification_sent" only contains one row maximum.';
1267 ----------------------------------------------
1268 -- Writing of history entries and event log --
1269 ----------------------------------------------
1272 CREATE FUNCTION "write_member_history_trigger"()
1273 RETURNS TRIGGER
1274 LANGUAGE 'plpgsql' VOLATILE AS $$
1275 BEGIN
1276 IF
1277 ( NEW."active" != OLD."active" OR
1278 NEW."name" != OLD."name" ) AND
1279 OLD."activated" NOTNULL
1280 THEN
1281 INSERT INTO "member_history"
1282 ("member_id", "active", "name")
1283 VALUES (NEW."id", OLD."active", OLD."name");
1284 END IF;
1285 RETURN NULL;
1286 END;
1287 $$;
1289 CREATE TRIGGER "write_member_history"
1290 AFTER UPDATE ON "member" FOR EACH ROW EXECUTE PROCEDURE
1291 "write_member_history_trigger"();
1293 COMMENT ON FUNCTION "write_member_history_trigger"() IS 'Implementation of trigger "write_member_history" on table "member"';
1294 COMMENT ON TRIGGER "write_member_history" ON "member" IS 'When changing certain fields of a member, create a history entry in "member_history" table';
1297 CREATE FUNCTION "write_event_issue_state_changed_trigger"()
1298 RETURNS TRIGGER
1299 LANGUAGE 'plpgsql' VOLATILE AS $$
1300 BEGIN
1301 IF NEW."state" != OLD."state" AND NEW."state" != 'calculation' THEN
1302 INSERT INTO "event" ("event", "issue_id", "state")
1303 VALUES ('issue_state_changed', NEW."id", NEW."state");
1304 END IF;
1305 RETURN NULL;
1306 END;
1307 $$;
1309 CREATE TRIGGER "write_event_issue_state_changed"
1310 AFTER UPDATE ON "issue" FOR EACH ROW EXECUTE PROCEDURE
1311 "write_event_issue_state_changed_trigger"();
1313 COMMENT ON FUNCTION "write_event_issue_state_changed_trigger"() IS 'Implementation of trigger "write_event_issue_state_changed" on table "issue"';
1314 COMMENT ON TRIGGER "write_event_issue_state_changed" ON "issue" IS 'Create entry in "event" table on "state" change';
1317 CREATE FUNCTION "write_event_initiative_or_draft_created_trigger"()
1318 RETURNS TRIGGER
1319 LANGUAGE 'plpgsql' VOLATILE AS $$
1320 DECLARE
1321 "initiative_row" "initiative"%ROWTYPE;
1322 "issue_row" "issue"%ROWTYPE;
1323 "event_v" "event_type";
1324 BEGIN
1325 SELECT * INTO "initiative_row" FROM "initiative"
1326 WHERE "id" = NEW."initiative_id";
1327 SELECT * INTO "issue_row" FROM "issue"
1328 WHERE "id" = "initiative_row"."issue_id";
1329 IF EXISTS (
1330 SELECT NULL FROM "draft"
1331 WHERE "initiative_id" = NEW."initiative_id"
1332 AND "id" != NEW."id"
1333 ) THEN
1334 "event_v" := 'new_draft_created';
1335 ELSE
1336 IF EXISTS (
1337 SELECT NULL FROM "initiative"
1338 WHERE "issue_id" = "initiative_row"."issue_id"
1339 AND "id" != "initiative_row"."id"
1340 ) THEN
1341 "event_v" := 'initiative_created_in_existing_issue';
1342 ELSE
1343 "event_v" := 'initiative_created_in_new_issue';
1344 END IF;
1345 END IF;
1346 INSERT INTO "event" (
1347 "event", "member_id",
1348 "issue_id", "state", "initiative_id", "draft_id"
1349 ) VALUES (
1350 "event_v",
1351 NEW."author_id",
1352 "initiative_row"."issue_id",
1353 "issue_row"."state",
1354 "initiative_row"."id",
1355 NEW."id" );
1356 RETURN NULL;
1357 END;
1358 $$;
1360 CREATE TRIGGER "write_event_initiative_or_draft_created"
1361 AFTER INSERT ON "draft" FOR EACH ROW EXECUTE PROCEDURE
1362 "write_event_initiative_or_draft_created_trigger"();
1364 COMMENT ON FUNCTION "write_event_initiative_or_draft_created_trigger"() IS 'Implementation of trigger "write_event_initiative_or_draft_created" on table "issue"';
1365 COMMENT ON TRIGGER "write_event_initiative_or_draft_created" ON "draft" IS 'Create entry in "event" table on draft creation';
1368 CREATE FUNCTION "write_event_initiative_revoked_trigger"()
1369 RETURNS TRIGGER
1370 LANGUAGE 'plpgsql' VOLATILE AS $$
1371 DECLARE
1372 "issue_row" "issue"%ROWTYPE;
1373 "draft_id_v" "draft"."id"%TYPE;
1374 BEGIN
1375 IF OLD."revoked" ISNULL AND NEW."revoked" NOTNULL THEN
1376 SELECT * INTO "issue_row" FROM "issue"
1377 WHERE "id" = NEW."issue_id";
1378 SELECT "id" INTO "draft_id_v" FROM "current_draft"
1379 WHERE "initiative_id" = NEW."id";
1380 INSERT INTO "event" (
1381 "event", "member_id", "issue_id", "state", "initiative_id", "draft_id"
1382 ) VALUES (
1383 'initiative_revoked',
1384 NEW."revoked_by_member_id",
1385 NEW."issue_id",
1386 "issue_row"."state",
1387 NEW."id",
1388 "draft_id_v");
1389 END IF;
1390 RETURN NULL;
1391 END;
1392 $$;
1394 CREATE TRIGGER "write_event_initiative_revoked"
1395 AFTER UPDATE ON "initiative" FOR EACH ROW EXECUTE PROCEDURE
1396 "write_event_initiative_revoked_trigger"();
1398 COMMENT ON FUNCTION "write_event_initiative_revoked_trigger"() IS 'Implementation of trigger "write_event_initiative_revoked" on table "issue"';
1399 COMMENT ON TRIGGER "write_event_initiative_revoked" ON "initiative" IS 'Create entry in "event" table, when an initiative is revoked';
1402 CREATE FUNCTION "write_event_suggestion_created_trigger"()
1403 RETURNS TRIGGER
1404 LANGUAGE 'plpgsql' VOLATILE AS $$
1405 DECLARE
1406 "initiative_row" "initiative"%ROWTYPE;
1407 "issue_row" "issue"%ROWTYPE;
1408 BEGIN
1409 SELECT * INTO "initiative_row" FROM "initiative"
1410 WHERE "id" = NEW."initiative_id";
1411 SELECT * INTO "issue_row" FROM "issue"
1412 WHERE "id" = "initiative_row"."issue_id";
1413 INSERT INTO "event" (
1414 "event", "member_id",
1415 "issue_id", "state", "initiative_id", "suggestion_id"
1416 ) VALUES (
1417 'suggestion_created',
1418 NEW."author_id",
1419 "initiative_row"."issue_id",
1420 "issue_row"."state",
1421 "initiative_row"."id",
1422 NEW."id" );
1423 RETURN NULL;
1424 END;
1425 $$;
1427 CREATE TRIGGER "write_event_suggestion_created"
1428 AFTER INSERT ON "suggestion" FOR EACH ROW EXECUTE PROCEDURE
1429 "write_event_suggestion_created_trigger"();
1431 COMMENT ON FUNCTION "write_event_suggestion_created_trigger"() IS 'Implementation of trigger "write_event_suggestion_created" on table "issue"';
1432 COMMENT ON TRIGGER "write_event_suggestion_created" ON "suggestion" IS 'Create entry in "event" table on suggestion creation';
1436 ----------------------------
1437 -- Additional constraints --
1438 ----------------------------
1441 CREATE FUNCTION "issue_requires_first_initiative_trigger"()
1442 RETURNS TRIGGER
1443 LANGUAGE 'plpgsql' VOLATILE AS $$
1444 BEGIN
1445 IF NOT EXISTS (
1446 SELECT NULL FROM "initiative" WHERE "issue_id" = NEW."id"
1447 ) THEN
1448 --RAISE 'Cannot create issue without an initial initiative.' USING
1449 -- ERRCODE = 'integrity_constraint_violation',
1450 -- HINT = 'Create issue, initiative, and draft within the same transaction.';
1451 RAISE EXCEPTION 'Cannot create issue without an initial initiative.';
1452 END IF;
1453 RETURN NULL;
1454 END;
1455 $$;
1457 CREATE CONSTRAINT TRIGGER "issue_requires_first_initiative"
1458 AFTER INSERT OR UPDATE ON "issue" DEFERRABLE INITIALLY DEFERRED
1459 FOR EACH ROW EXECUTE PROCEDURE
1460 "issue_requires_first_initiative_trigger"();
1462 COMMENT ON FUNCTION "issue_requires_first_initiative_trigger"() IS 'Implementation of trigger "issue_requires_first_initiative" on table "issue"';
1463 COMMENT ON TRIGGER "issue_requires_first_initiative" ON "issue" IS 'Ensure that new issues have at least one initiative';
1466 CREATE FUNCTION "last_initiative_deletes_issue_trigger"()
1467 RETURNS TRIGGER
1468 LANGUAGE 'plpgsql' VOLATILE AS $$
1469 DECLARE
1470 "reference_lost" BOOLEAN;
1471 BEGIN
1472 IF TG_OP = 'DELETE' THEN
1473 "reference_lost" := TRUE;
1474 ELSE
1475 "reference_lost" := NEW."issue_id" != OLD."issue_id";
1476 END IF;
1477 IF
1478 "reference_lost" AND NOT EXISTS (
1479 SELECT NULL FROM "initiative" WHERE "issue_id" = OLD."issue_id"
1480 )
1481 THEN
1482 DELETE FROM "issue" WHERE "id" = OLD."issue_id";
1483 END IF;
1484 RETURN NULL;
1485 END;
1486 $$;
1488 CREATE CONSTRAINT TRIGGER "last_initiative_deletes_issue"
1489 AFTER UPDATE OR DELETE ON "initiative" DEFERRABLE INITIALLY DEFERRED
1490 FOR EACH ROW EXECUTE PROCEDURE
1491 "last_initiative_deletes_issue_trigger"();
1493 COMMENT ON FUNCTION "last_initiative_deletes_issue_trigger"() IS 'Implementation of trigger "last_initiative_deletes_issue" on table "initiative"';
1494 COMMENT ON TRIGGER "last_initiative_deletes_issue" ON "initiative" IS 'Removing the last initiative of an issue deletes the issue';
1497 CREATE FUNCTION "initiative_requires_first_draft_trigger"()
1498 RETURNS TRIGGER
1499 LANGUAGE 'plpgsql' VOLATILE AS $$
1500 BEGIN
1501 IF NOT EXISTS (
1502 SELECT NULL FROM "draft" WHERE "initiative_id" = NEW."id"
1503 ) THEN
1504 --RAISE 'Cannot create initiative without an initial draft.' USING
1505 -- ERRCODE = 'integrity_constraint_violation',
1506 -- HINT = 'Create issue, initiative and draft within the same transaction.';
1507 RAISE EXCEPTION 'Cannot create initiative without an initial draft.';
1508 END IF;
1509 RETURN NULL;
1510 END;
1511 $$;
1513 CREATE CONSTRAINT TRIGGER "initiative_requires_first_draft"
1514 AFTER INSERT OR UPDATE ON "initiative" DEFERRABLE INITIALLY DEFERRED
1515 FOR EACH ROW EXECUTE PROCEDURE
1516 "initiative_requires_first_draft_trigger"();
1518 COMMENT ON FUNCTION "initiative_requires_first_draft_trigger"() IS 'Implementation of trigger "initiative_requires_first_draft" on table "initiative"';
1519 COMMENT ON TRIGGER "initiative_requires_first_draft" ON "initiative" IS 'Ensure that new initiatives have at least one draft';
1522 CREATE FUNCTION "last_draft_deletes_initiative_trigger"()
1523 RETURNS TRIGGER
1524 LANGUAGE 'plpgsql' VOLATILE AS $$
1525 DECLARE
1526 "reference_lost" BOOLEAN;
1527 BEGIN
1528 IF TG_OP = 'DELETE' THEN
1529 "reference_lost" := TRUE;
1530 ELSE
1531 "reference_lost" := NEW."initiative_id" != OLD."initiative_id";
1532 END IF;
1533 IF
1534 "reference_lost" AND NOT EXISTS (
1535 SELECT NULL FROM "draft" WHERE "initiative_id" = OLD."initiative_id"
1536 )
1537 THEN
1538 DELETE FROM "initiative" WHERE "id" = OLD."initiative_id";
1539 END IF;
1540 RETURN NULL;
1541 END;
1542 $$;
1544 CREATE CONSTRAINT TRIGGER "last_draft_deletes_initiative"
1545 AFTER UPDATE OR DELETE ON "draft" DEFERRABLE INITIALLY DEFERRED
1546 FOR EACH ROW EXECUTE PROCEDURE
1547 "last_draft_deletes_initiative_trigger"();
1549 COMMENT ON FUNCTION "last_draft_deletes_initiative_trigger"() IS 'Implementation of trigger "last_draft_deletes_initiative" on table "draft"';
1550 COMMENT ON TRIGGER "last_draft_deletes_initiative" ON "draft" IS 'Removing the last draft of an initiative deletes the initiative';
1553 CREATE FUNCTION "suggestion_requires_first_opinion_trigger"()
1554 RETURNS TRIGGER
1555 LANGUAGE 'plpgsql' VOLATILE AS $$
1556 BEGIN
1557 IF NOT EXISTS (
1558 SELECT NULL FROM "opinion" WHERE "suggestion_id" = NEW."id"
1559 ) THEN
1560 RAISE EXCEPTION 'Cannot create a suggestion without an opinion.';
1561 END IF;
1562 RETURN NULL;
1563 END;
1564 $$;
1566 CREATE CONSTRAINT TRIGGER "suggestion_requires_first_opinion"
1567 AFTER INSERT OR UPDATE ON "suggestion" DEFERRABLE INITIALLY DEFERRED
1568 FOR EACH ROW EXECUTE PROCEDURE
1569 "suggestion_requires_first_opinion_trigger"();
1571 COMMENT ON FUNCTION "suggestion_requires_first_opinion_trigger"() IS 'Implementation of trigger "suggestion_requires_first_opinion" on table "suggestion"';
1572 COMMENT ON TRIGGER "suggestion_requires_first_opinion" ON "suggestion" IS 'Ensure that new suggestions have at least one opinion';
1575 CREATE FUNCTION "last_opinion_deletes_suggestion_trigger"()
1576 RETURNS TRIGGER
1577 LANGUAGE 'plpgsql' VOLATILE AS $$
1578 DECLARE
1579 "reference_lost" BOOLEAN;
1580 BEGIN
1581 IF TG_OP = 'DELETE' THEN
1582 "reference_lost" := TRUE;
1583 ELSE
1584 "reference_lost" := NEW."suggestion_id" != OLD."suggestion_id";
1585 END IF;
1586 IF
1587 "reference_lost" AND NOT EXISTS (
1588 SELECT NULL FROM "opinion" WHERE "suggestion_id" = OLD."suggestion_id"
1589 )
1590 THEN
1591 DELETE FROM "suggestion" WHERE "id" = OLD."suggestion_id";
1592 END IF;
1593 RETURN NULL;
1594 END;
1595 $$;
1597 CREATE CONSTRAINT TRIGGER "last_opinion_deletes_suggestion"
1598 AFTER UPDATE OR DELETE ON "opinion" DEFERRABLE INITIALLY DEFERRED
1599 FOR EACH ROW EXECUTE PROCEDURE
1600 "last_opinion_deletes_suggestion_trigger"();
1602 COMMENT ON FUNCTION "last_opinion_deletes_suggestion_trigger"() IS 'Implementation of trigger "last_opinion_deletes_suggestion" on table "opinion"';
1603 COMMENT ON TRIGGER "last_opinion_deletes_suggestion" ON "opinion" IS 'Removing the last opinion of a suggestion deletes the suggestion';
1607 ---------------------------------------------------------------
1608 -- Ensure that votes are not modified when issues are frozen --
1609 ---------------------------------------------------------------
1611 -- NOTE: Frontends should ensure this anyway, but in case of programming
1612 -- errors the following triggers ensure data integrity.
1615 CREATE FUNCTION "forbid_changes_on_closed_issue_trigger"()
1616 RETURNS TRIGGER
1617 LANGUAGE 'plpgsql' VOLATILE AS $$
1618 DECLARE
1619 "issue_id_v" "issue"."id"%TYPE;
1620 "issue_row" "issue"%ROWTYPE;
1621 BEGIN
1622 IF TG_OP = 'DELETE' THEN
1623 "issue_id_v" := OLD."issue_id";
1624 ELSE
1625 "issue_id_v" := NEW."issue_id";
1626 END IF;
1627 SELECT INTO "issue_row" * FROM "issue"
1628 WHERE "id" = "issue_id_v" FOR SHARE;
1629 IF "issue_row"."closed" NOTNULL THEN
1630 RAISE EXCEPTION 'Tried to modify data belonging to a closed issue.';
1631 END IF;
1632 RETURN NULL;
1633 END;
1634 $$;
1636 CREATE TRIGGER "forbid_changes_on_closed_issue"
1637 AFTER INSERT OR UPDATE OR DELETE ON "direct_voter"
1638 FOR EACH ROW EXECUTE PROCEDURE
1639 "forbid_changes_on_closed_issue_trigger"();
1641 CREATE TRIGGER "forbid_changes_on_closed_issue"
1642 AFTER INSERT OR UPDATE OR DELETE ON "delegating_voter"
1643 FOR EACH ROW EXECUTE PROCEDURE
1644 "forbid_changes_on_closed_issue_trigger"();
1646 CREATE TRIGGER "forbid_changes_on_closed_issue"
1647 AFTER INSERT OR UPDATE OR DELETE ON "vote"
1648 FOR EACH ROW EXECUTE PROCEDURE
1649 "forbid_changes_on_closed_issue_trigger"();
1651 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"';
1652 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';
1653 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';
1654 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';
1658 --------------------------------------------------------------------
1659 -- Auto-retrieval of fields only needed for referential integrity --
1660 --------------------------------------------------------------------
1663 CREATE FUNCTION "autofill_issue_id_trigger"()
1664 RETURNS TRIGGER
1665 LANGUAGE 'plpgsql' VOLATILE AS $$
1666 BEGIN
1667 IF NEW."issue_id" ISNULL THEN
1668 SELECT "issue_id" INTO NEW."issue_id"
1669 FROM "initiative" WHERE "id" = NEW."initiative_id";
1670 END IF;
1671 RETURN NEW;
1672 END;
1673 $$;
1675 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "supporter"
1676 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
1678 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "vote"
1679 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
1681 COMMENT ON FUNCTION "autofill_issue_id_trigger"() IS 'Implementation of triggers "autofill_issue_id" on tables "supporter" and "vote"';
1682 COMMENT ON TRIGGER "autofill_issue_id" ON "supporter" IS 'Set "issue_id" field automatically, if NULL';
1683 COMMENT ON TRIGGER "autofill_issue_id" ON "vote" IS 'Set "issue_id" field automatically, if NULL';
1686 CREATE FUNCTION "autofill_initiative_id_trigger"()
1687 RETURNS TRIGGER
1688 LANGUAGE 'plpgsql' VOLATILE AS $$
1689 BEGIN
1690 IF NEW."initiative_id" ISNULL THEN
1691 SELECT "initiative_id" INTO NEW."initiative_id"
1692 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
1693 END IF;
1694 RETURN NEW;
1695 END;
1696 $$;
1698 CREATE TRIGGER "autofill_initiative_id" BEFORE INSERT ON "opinion"
1699 FOR EACH ROW EXECUTE PROCEDURE "autofill_initiative_id_trigger"();
1701 COMMENT ON FUNCTION "autofill_initiative_id_trigger"() IS 'Implementation of trigger "autofill_initiative_id" on table "opinion"';
1702 COMMENT ON TRIGGER "autofill_initiative_id" ON "opinion" IS 'Set "initiative_id" field automatically, if NULL';
1706 -----------------------------------------------------
1707 -- Automatic calculation of certain default values --
1708 -----------------------------------------------------
1711 CREATE FUNCTION "copy_timings_trigger"()
1712 RETURNS TRIGGER
1713 LANGUAGE 'plpgsql' VOLATILE AS $$
1714 DECLARE
1715 "policy_row" "policy"%ROWTYPE;
1716 BEGIN
1717 SELECT * INTO "policy_row" FROM "policy"
1718 WHERE "id" = NEW."policy_id";
1719 IF NEW."admission_time" ISNULL THEN
1720 NEW."admission_time" := "policy_row"."admission_time";
1721 END IF;
1722 IF NEW."discussion_time" ISNULL THEN
1723 NEW."discussion_time" := "policy_row"."discussion_time";
1724 END IF;
1725 IF NEW."verification_time" ISNULL THEN
1726 NEW."verification_time" := "policy_row"."verification_time";
1727 END IF;
1728 IF NEW."voting_time" ISNULL THEN
1729 NEW."voting_time" := "policy_row"."voting_time";
1730 END IF;
1731 RETURN NEW;
1732 END;
1733 $$;
1735 CREATE TRIGGER "copy_timings" BEFORE INSERT OR UPDATE ON "issue"
1736 FOR EACH ROW EXECUTE PROCEDURE "copy_timings_trigger"();
1738 COMMENT ON FUNCTION "copy_timings_trigger"() IS 'Implementation of trigger "copy_timings" on table "issue"';
1739 COMMENT ON TRIGGER "copy_timings" ON "issue" IS 'If timing fields are NULL, copy values from policy.';
1742 CREATE FUNCTION "default_for_draft_id_trigger"()
1743 RETURNS TRIGGER
1744 LANGUAGE 'plpgsql' VOLATILE AS $$
1745 BEGIN
1746 IF NEW."draft_id" ISNULL THEN
1747 SELECT "id" INTO NEW."draft_id" FROM "current_draft"
1748 WHERE "initiative_id" = NEW."initiative_id";
1749 END IF;
1750 RETURN NEW;
1751 END;
1752 $$;
1754 CREATE TRIGGER "default_for_draft_id" BEFORE INSERT OR UPDATE ON "suggestion"
1755 FOR EACH ROW EXECUTE PROCEDURE "default_for_draft_id_trigger"();
1756 CREATE TRIGGER "default_for_draft_id" BEFORE INSERT OR UPDATE ON "supporter"
1757 FOR EACH ROW EXECUTE PROCEDURE "default_for_draft_id_trigger"();
1759 COMMENT ON FUNCTION "default_for_draft_id_trigger"() IS 'Implementation of trigger "default_for_draft" on tables "supporter" and "suggestion"';
1760 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';
1761 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';
1765 ----------------------------------------
1766 -- Automatic creation of dependencies --
1767 ----------------------------------------
1770 CREATE FUNCTION "autocreate_interest_trigger"()
1771 RETURNS TRIGGER
1772 LANGUAGE 'plpgsql' VOLATILE AS $$
1773 BEGIN
1774 IF NOT EXISTS (
1775 SELECT NULL FROM "initiative" JOIN "interest"
1776 ON "initiative"."issue_id" = "interest"."issue_id"
1777 WHERE "initiative"."id" = NEW."initiative_id"
1778 AND "interest"."member_id" = NEW."member_id"
1779 ) THEN
1780 BEGIN
1781 INSERT INTO "interest" ("issue_id", "member_id")
1782 SELECT "issue_id", NEW."member_id"
1783 FROM "initiative" WHERE "id" = NEW."initiative_id";
1784 EXCEPTION WHEN unique_violation THEN END;
1785 END IF;
1786 RETURN NEW;
1787 END;
1788 $$;
1790 CREATE TRIGGER "autocreate_interest" BEFORE INSERT ON "supporter"
1791 FOR EACH ROW EXECUTE PROCEDURE "autocreate_interest_trigger"();
1793 COMMENT ON FUNCTION "autocreate_interest_trigger"() IS 'Implementation of trigger "autocreate_interest" on table "supporter"';
1794 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';
1797 CREATE FUNCTION "autocreate_supporter_trigger"()
1798 RETURNS TRIGGER
1799 LANGUAGE 'plpgsql' VOLATILE AS $$
1800 BEGIN
1801 IF NOT EXISTS (
1802 SELECT NULL FROM "suggestion" JOIN "supporter"
1803 ON "suggestion"."initiative_id" = "supporter"."initiative_id"
1804 WHERE "suggestion"."id" = NEW."suggestion_id"
1805 AND "supporter"."member_id" = NEW."member_id"
1806 ) THEN
1807 BEGIN
1808 INSERT INTO "supporter" ("initiative_id", "member_id")
1809 SELECT "initiative_id", NEW."member_id"
1810 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
1811 EXCEPTION WHEN unique_violation THEN END;
1812 END IF;
1813 RETURN NEW;
1814 END;
1815 $$;
1817 CREATE TRIGGER "autocreate_supporter" BEFORE INSERT ON "opinion"
1818 FOR EACH ROW EXECUTE PROCEDURE "autocreate_supporter_trigger"();
1820 COMMENT ON FUNCTION "autocreate_supporter_trigger"() IS 'Implementation of trigger "autocreate_supporter" on table "opinion"';
1821 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.';
1825 ------------------------------------------
1826 -- Views and helper functions for views --
1827 ------------------------------------------
1830 CREATE VIEW "unit_delegation" AS
1831 SELECT
1832 "unit"."id" AS "unit_id",
1833 "delegation"."id",
1834 "delegation"."truster_id",
1835 "delegation"."trustee_id",
1836 "delegation"."scope"
1837 FROM "unit"
1838 JOIN "delegation"
1839 ON "delegation"."unit_id" = "unit"."id"
1840 JOIN "member"
1841 ON "delegation"."truster_id" = "member"."id"
1842 JOIN "privilege"
1843 ON "delegation"."unit_id" = "privilege"."unit_id"
1844 AND "delegation"."truster_id" = "privilege"."member_id"
1845 WHERE "member"."active" AND "privilege"."voting_right";
1847 COMMENT ON VIEW "unit_delegation" IS 'Unit delegations where trusters are active and have voting right';
1850 CREATE VIEW "area_delegation" AS
1851 SELECT DISTINCT ON ("area"."id", "delegation"."truster_id")
1852 "area"."id" AS "area_id",
1853 "delegation"."id",
1854 "delegation"."truster_id",
1855 "delegation"."trustee_id",
1856 "delegation"."scope"
1857 FROM "area"
1858 JOIN "delegation"
1859 ON "delegation"."unit_id" = "area"."unit_id"
1860 OR "delegation"."area_id" = "area"."id"
1861 JOIN "member"
1862 ON "delegation"."truster_id" = "member"."id"
1863 JOIN "privilege"
1864 ON "area"."unit_id" = "privilege"."unit_id"
1865 AND "delegation"."truster_id" = "privilege"."member_id"
1866 WHERE "member"."active" AND "privilege"."voting_right"
1867 ORDER BY
1868 "area"."id",
1869 "delegation"."truster_id",
1870 "delegation"."scope" DESC;
1872 COMMENT ON VIEW "area_delegation" IS 'Area delegations where trusters are active and have voting right';
1875 CREATE VIEW "issue_delegation" AS
1876 SELECT DISTINCT ON ("issue"."id", "delegation"."truster_id")
1877 "issue"."id" AS "issue_id",
1878 "delegation"."id",
1879 "delegation"."truster_id",
1880 "delegation"."trustee_id",
1881 "delegation"."scope"
1882 FROM "issue"
1883 JOIN "area"
1884 ON "area"."id" = "issue"."area_id"
1885 JOIN "delegation"
1886 ON "delegation"."unit_id" = "area"."unit_id"
1887 OR "delegation"."area_id" = "area"."id"
1888 OR "delegation"."issue_id" = "issue"."id"
1889 JOIN "member"
1890 ON "delegation"."truster_id" = "member"."id"
1891 JOIN "privilege"
1892 ON "area"."unit_id" = "privilege"."unit_id"
1893 AND "delegation"."truster_id" = "privilege"."member_id"
1894 WHERE "member"."active" AND "privilege"."voting_right"
1895 ORDER BY
1896 "issue"."id",
1897 "delegation"."truster_id",
1898 "delegation"."scope" DESC;
1900 COMMENT ON VIEW "issue_delegation" IS 'Issue delegations where trusters are active and have voting right';
1903 CREATE FUNCTION "membership_weight_with_skipping"
1904 ( "area_id_p" "area"."id"%TYPE,
1905 "member_id_p" "member"."id"%TYPE,
1906 "skip_member_ids_p" INT4[] ) -- "member"."id"%TYPE[]
1907 RETURNS INT4
1908 LANGUAGE 'plpgsql' STABLE AS $$
1909 DECLARE
1910 "sum_v" INT4;
1911 "delegation_row" "area_delegation"%ROWTYPE;
1912 BEGIN
1913 "sum_v" := 1;
1914 FOR "delegation_row" IN
1915 SELECT "area_delegation".*
1916 FROM "area_delegation" LEFT JOIN "membership"
1917 ON "membership"."area_id" = "area_id_p"
1918 AND "membership"."member_id" = "area_delegation"."truster_id"
1919 WHERE "area_delegation"."area_id" = "area_id_p"
1920 AND "area_delegation"."trustee_id" = "member_id_p"
1921 AND "membership"."member_id" ISNULL
1922 LOOP
1923 IF NOT
1924 "skip_member_ids_p" @> ARRAY["delegation_row"."truster_id"]
1925 THEN
1926 "sum_v" := "sum_v" + "membership_weight_with_skipping"(
1927 "area_id_p",
1928 "delegation_row"."truster_id",
1929 "skip_member_ids_p" || "delegation_row"."truster_id"
1930 );
1931 END IF;
1932 END LOOP;
1933 RETURN "sum_v";
1934 END;
1935 $$;
1937 COMMENT ON FUNCTION "membership_weight_with_skipping"
1938 ( "area"."id"%TYPE,
1939 "member"."id"%TYPE,
1940 INT4[] )
1941 IS 'Helper function for "membership_weight" function';
1944 CREATE FUNCTION "membership_weight"
1945 ( "area_id_p" "area"."id"%TYPE,
1946 "member_id_p" "member"."id"%TYPE ) -- "member"."id"%TYPE[]
1947 RETURNS INT4
1948 LANGUAGE 'plpgsql' STABLE AS $$
1949 BEGIN
1950 RETURN "membership_weight_with_skipping"(
1951 "area_id_p",
1952 "member_id_p",
1953 ARRAY["member_id_p"]
1954 );
1955 END;
1956 $$;
1958 COMMENT ON FUNCTION "membership_weight"
1959 ( "area"."id"%TYPE,
1960 "member"."id"%TYPE )
1961 IS 'Calculates the potential voting weight of a member in a given area';
1964 CREATE VIEW "member_count_view" AS
1965 SELECT count(1) AS "total_count" FROM "member" WHERE "active";
1967 COMMENT ON VIEW "member_count_view" IS 'View used to update "member_count" table';
1970 CREATE VIEW "unit_member_count" AS
1971 SELECT
1972 "unit"."id" AS "unit_id",
1973 count("member"."id") AS "member_count"
1974 FROM "unit"
1975 LEFT JOIN "privilege"
1976 ON "privilege"."unit_id" = "unit"."id"
1977 AND "privilege"."voting_right"
1978 LEFT JOIN "member"
1979 ON "member"."id" = "privilege"."member_id"
1980 AND "member"."active"
1981 GROUP BY "unit"."id";
1983 COMMENT ON VIEW "unit_member_count" IS 'View used to update "member_count" column of "unit" table';
1986 CREATE VIEW "area_member_count" AS
1987 SELECT
1988 "area"."id" AS "area_id",
1989 count("member"."id") AS "direct_member_count",
1990 coalesce(
1991 sum(
1992 CASE WHEN "member"."id" NOTNULL THEN
1993 "membership_weight"("area"."id", "member"."id")
1994 ELSE 0 END
1995 )
1996 ) AS "member_weight"
1997 FROM "area"
1998 LEFT JOIN "membership"
1999 ON "area"."id" = "membership"."area_id"
2000 LEFT JOIN "privilege"
2001 ON "privilege"."unit_id" = "area"."unit_id"
2002 AND "privilege"."member_id" = "membership"."member_id"
2003 AND "privilege"."voting_right"
2004 LEFT JOIN "member"
2005 ON "member"."id" = "privilege"."member_id" -- NOTE: no membership here!
2006 AND "member"."active"
2007 GROUP BY "area"."id";
2009 COMMENT ON VIEW "area_member_count" IS 'View used to update "direct_member_count" and "member_weight" columns of table "area"';
2012 CREATE VIEW "opening_draft" AS
2013 SELECT "draft".* FROM (
2014 SELECT
2015 "initiative"."id" AS "initiative_id",
2016 min("draft"."id") AS "draft_id"
2017 FROM "initiative" JOIN "draft"
2018 ON "initiative"."id" = "draft"."initiative_id"
2019 GROUP BY "initiative"."id"
2020 ) AS "subquery"
2021 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
2023 COMMENT ON VIEW "opening_draft" IS 'First drafts of all initiatives';
2026 CREATE VIEW "current_draft" AS
2027 SELECT "draft".* FROM (
2028 SELECT
2029 "initiative"."id" AS "initiative_id",
2030 max("draft"."id") AS "draft_id"
2031 FROM "initiative" JOIN "draft"
2032 ON "initiative"."id" = "draft"."initiative_id"
2033 GROUP BY "initiative"."id"
2034 ) AS "subquery"
2035 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
2037 COMMENT ON VIEW "current_draft" IS 'All latest drafts for each initiative';
2040 CREATE VIEW "critical_opinion" AS
2041 SELECT * FROM "opinion"
2042 WHERE ("degree" = 2 AND "fulfilled" = FALSE)
2043 OR ("degree" = -2 AND "fulfilled" = TRUE);
2045 COMMENT ON VIEW "critical_opinion" IS 'Opinions currently causing dissatisfaction';
2048 CREATE VIEW "battle_participant" AS
2049 SELECT "initiative"."id", "initiative"."issue_id"
2050 FROM "issue" JOIN "initiative"
2051 ON "issue"."id" = "initiative"."issue_id"
2052 WHERE "initiative"."admitted"
2053 UNION ALL
2054 SELECT NULL, "id" AS "issue_id"
2055 FROM "issue";
2057 COMMENT ON VIEW "battle_participant" IS 'Helper view for "battle_view" containing admitted initiatives plus virtual "status-quo" initiative denoted by NULL reference';
2060 CREATE VIEW "battle_view" AS
2061 SELECT
2062 "issue"."id" AS "issue_id",
2063 "winning_initiative"."id" AS "winning_initiative_id",
2064 "losing_initiative"."id" AS "losing_initiative_id",
2065 sum(
2066 CASE WHEN
2067 coalesce("better_vote"."grade", 0) >
2068 coalesce("worse_vote"."grade", 0)
2069 THEN "direct_voter"."weight" ELSE 0 END
2070 ) AS "count"
2071 FROM "issue"
2072 LEFT JOIN "direct_voter"
2073 ON "issue"."id" = "direct_voter"."issue_id"
2074 JOIN "battle_participant" AS "winning_initiative"
2075 ON "issue"."id" = "winning_initiative"."issue_id"
2076 JOIN "battle_participant" AS "losing_initiative"
2077 ON "issue"."id" = "losing_initiative"."issue_id"
2078 LEFT JOIN "vote" AS "better_vote"
2079 ON "direct_voter"."member_id" = "better_vote"."member_id"
2080 AND "winning_initiative"."id" = "better_vote"."initiative_id"
2081 LEFT JOIN "vote" AS "worse_vote"
2082 ON "direct_voter"."member_id" = "worse_vote"."member_id"
2083 AND "losing_initiative"."id" = "worse_vote"."initiative_id"
2084 WHERE "issue"."closed" NOTNULL
2085 AND "issue"."cleaned" ISNULL
2086 AND (
2087 "winning_initiative"."id" != "losing_initiative"."id" OR
2088 ( ("winning_initiative"."id" NOTNULL AND "losing_initiative"."id" ISNULL) OR
2089 ("winning_initiative"."id" ISNULL AND "losing_initiative"."id" NOTNULL) ) )
2090 GROUP BY
2091 "issue"."id",
2092 "winning_initiative"."id",
2093 "losing_initiative"."id";
2095 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';
2098 CREATE VIEW "expired_session" AS
2099 SELECT * FROM "session" WHERE now() > "expiry";
2101 CREATE RULE "delete" AS ON DELETE TO "expired_session" DO INSTEAD
2102 DELETE FROM "session" WHERE "ident" = OLD."ident";
2104 COMMENT ON VIEW "expired_session" IS 'View containing all expired sessions where DELETE is possible';
2105 COMMENT ON RULE "delete" ON "expired_session" IS 'Rule allowing DELETE on rows in "expired_session" view, i.e. DELETE FROM "expired_session"';
2108 CREATE VIEW "open_issue" AS
2109 SELECT * FROM "issue" WHERE "closed" ISNULL;
2111 COMMENT ON VIEW "open_issue" IS 'All open issues';
2114 CREATE VIEW "issue_with_ranks_missing" AS
2115 SELECT * FROM "issue"
2116 WHERE "fully_frozen" NOTNULL
2117 AND "closed" NOTNULL
2118 AND "ranks_available" = FALSE;
2120 COMMENT ON VIEW "issue_with_ranks_missing" IS 'Issues where voting was finished, but no ranks have been calculated yet';
2123 CREATE VIEW "member_contingent" AS
2124 SELECT
2125 "member"."id" AS "member_id",
2126 "contingent"."time_frame",
2127 CASE WHEN "contingent"."text_entry_limit" NOTNULL THEN
2128 (
2129 SELECT count(1) FROM "draft"
2130 WHERE "draft"."author_id" = "member"."id"
2131 AND "draft"."created" > now() - "contingent"."time_frame"
2132 ) + (
2133 SELECT count(1) FROM "suggestion"
2134 WHERE "suggestion"."author_id" = "member"."id"
2135 AND "suggestion"."created" > now() - "contingent"."time_frame"
2136 )
2137 ELSE NULL END AS "text_entry_count",
2138 "contingent"."text_entry_limit",
2139 CASE WHEN "contingent"."initiative_limit" NOTNULL THEN (
2140 SELECT count(1) FROM "opening_draft"
2141 WHERE "opening_draft"."author_id" = "member"."id"
2142 AND "opening_draft"."created" > now() - "contingent"."time_frame"
2143 ) ELSE NULL END AS "initiative_count",
2144 "contingent"."initiative_limit"
2145 FROM "member" CROSS JOIN "contingent";
2147 COMMENT ON VIEW "member_contingent" IS 'Actual counts of text entries and initiatives are calculated per member for each limit in the "contingent" table.';
2149 COMMENT ON COLUMN "member_contingent"."text_entry_count" IS 'Only calculated when "text_entry_limit" is not null in the same row';
2150 COMMENT ON COLUMN "member_contingent"."initiative_count" IS 'Only calculated when "initiative_limit" is not null in the same row';
2153 CREATE VIEW "member_contingent_left" AS
2154 SELECT
2155 "member_id",
2156 max("text_entry_limit" - "text_entry_count") AS "text_entries_left",
2157 max("initiative_limit" - "initiative_count") AS "initiatives_left"
2158 FROM "member_contingent" GROUP BY "member_id";
2160 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.';
2163 CREATE VIEW "event_seen_by_member" AS
2164 SELECT
2165 "member"."id" AS "seen_by_member_id",
2166 CASE WHEN "event"."state" IN (
2167 'voting',
2168 'finished_without_winner',
2169 'finished_with_winner'
2170 ) THEN
2171 'voting'::"notify_level"
2172 ELSE
2173 CASE WHEN "event"."state" IN (
2174 'verification',
2175 'canceled_after_revocation_during_verification',
2176 'canceled_no_initiative_admitted'
2177 ) THEN
2178 'verification'::"notify_level"
2179 ELSE
2180 CASE WHEN "event"."state" IN (
2181 'discussion',
2182 'canceled_after_revocation_during_discussion'
2183 ) THEN
2184 'discussion'::"notify_level"
2185 ELSE
2186 'all'::"notify_level"
2187 END
2188 END
2189 END AS "notify_level",
2190 "event".*
2191 FROM "member" CROSS JOIN "event"
2192 LEFT JOIN "issue"
2193 ON "event"."issue_id" = "issue"."id"
2194 LEFT JOIN "membership"
2195 ON "member"."id" = "membership"."member_id"
2196 AND "issue"."area_id" = "membership"."area_id"
2197 LEFT JOIN "interest"
2198 ON "member"."id" = "interest"."member_id"
2199 AND "event"."issue_id" = "interest"."issue_id"
2200 LEFT JOIN "supporter"
2201 ON "member"."id" = "supporter"."member_id"
2202 AND "event"."initiative_id" = "supporter"."initiative_id"
2203 LEFT JOIN "ignored_member"
2204 ON "member"."id" = "ignored_member"."member_id"
2205 AND "event"."member_id" = "ignored_member"."other_member_id"
2206 LEFT JOIN "ignored_initiative"
2207 ON "member"."id" = "ignored_initiative"."member_id"
2208 AND "event"."initiative_id" = "ignored_initiative"."initiative_id"
2209 WHERE (
2210 "supporter"."member_id" NOTNULL OR
2211 "interest"."member_id" NOTNULL OR
2212 ( "membership"."member_id" NOTNULL AND
2213 "event"."event" IN (
2214 'issue_state_changed',
2215 'initiative_created_in_new_issue',
2216 'initiative_created_in_existing_issue',
2217 'initiative_revoked' ) ) )
2218 AND "ignored_member"."member_id" ISNULL
2219 AND "ignored_initiative"."member_id" ISNULL;
2221 COMMENT ON VIEW "event_seen_by_member" IS 'Events as seen by a member, depending on its memberships, interests and support, but ignoring members "notify_level"';
2224 CREATE VIEW "selected_event_seen_by_member" AS
2225 SELECT
2226 "member"."id" AS "seen_by_member_id",
2227 CASE WHEN "event"."state" IN (
2228 'voting',
2229 'finished_without_winner',
2230 'finished_with_winner'
2231 ) THEN
2232 'voting'::"notify_level"
2233 ELSE
2234 CASE WHEN "event"."state" IN (
2235 'verification',
2236 'canceled_after_revocation_during_verification',
2237 'canceled_no_initiative_admitted'
2238 ) THEN
2239 'verification'::"notify_level"
2240 ELSE
2241 CASE WHEN "event"."state" IN (
2242 'discussion',
2243 'canceled_after_revocation_during_discussion'
2244 ) THEN
2245 'discussion'::"notify_level"
2246 ELSE
2247 'all'::"notify_level"
2248 END
2249 END
2250 END AS "notify_level",
2251 "event".*
2252 FROM "member" CROSS JOIN "event"
2253 LEFT JOIN "issue"
2254 ON "event"."issue_id" = "issue"."id"
2255 LEFT JOIN "membership"
2256 ON "member"."id" = "membership"."member_id"
2257 AND "issue"."area_id" = "membership"."area_id"
2258 LEFT JOIN "interest"
2259 ON "member"."id" = "interest"."member_id"
2260 AND "event"."issue_id" = "interest"."issue_id"
2261 LEFT JOIN "supporter"
2262 ON "member"."id" = "supporter"."member_id"
2263 AND "event"."initiative_id" = "supporter"."initiative_id"
2264 LEFT JOIN "ignored_member"
2265 ON "member"."id" = "ignored_member"."member_id"
2266 AND "event"."member_id" = "ignored_member"."other_member_id"
2267 LEFT JOIN "ignored_initiative"
2268 ON "member"."id" = "ignored_initiative"."member_id"
2269 AND "event"."initiative_id" = "ignored_initiative"."initiative_id"
2270 WHERE (
2271 ( "member"."notify_level" >= 'all' ) OR
2272 ( "member"."notify_level" >= 'voting' AND
2273 "event"."state" IN (
2274 'voting',
2275 'finished_without_winner',
2276 'finished_with_winner' ) ) OR
2277 ( "member"."notify_level" >= 'verification' AND
2278 "event"."state" IN (
2279 'verification',
2280 'canceled_after_revocation_during_verification',
2281 'canceled_no_initiative_admitted' ) ) OR
2282 ( "member"."notify_level" >= 'discussion' AND
2283 "event"."state" IN (
2284 'discussion',
2285 'canceled_after_revocation_during_discussion' ) ) )
2286 AND (
2287 "supporter"."member_id" NOTNULL OR
2288 "interest"."member_id" NOTNULL OR
2289 ( "membership"."member_id" NOTNULL AND
2290 "event"."event" IN (
2291 'issue_state_changed',
2292 'initiative_created_in_new_issue',
2293 'initiative_created_in_existing_issue',
2294 'initiative_revoked' ) ) )
2295 AND "ignored_member"."member_id" ISNULL
2296 AND "ignored_initiative"."member_id" ISNULL;
2298 COMMENT ON VIEW "selected_event_seen_by_member" IS 'Events as seen by a member, depending on its memberships, interests, support and members "notify_level"';
2301 CREATE TYPE "timeline_event" AS ENUM (
2302 'issue_created',
2303 'issue_canceled',
2304 'issue_accepted',
2305 'issue_half_frozen',
2306 'issue_finished_without_voting',
2307 'issue_voting_started',
2308 'issue_finished_after_voting',
2309 'initiative_created',
2310 'initiative_revoked',
2311 'draft_created',
2312 'suggestion_created');
2314 COMMENT ON TYPE "timeline_event" IS 'Types of event in timeline tables (DEPRECATED)';
2317 CREATE VIEW "timeline_issue" AS
2318 SELECT
2319 "created" AS "occurrence",
2320 'issue_created'::"timeline_event" AS "event",
2321 "id" AS "issue_id"
2322 FROM "issue"
2323 UNION ALL
2324 SELECT
2325 "closed" AS "occurrence",
2326 'issue_canceled'::"timeline_event" AS "event",
2327 "id" AS "issue_id"
2328 FROM "issue" WHERE "closed" NOTNULL AND "fully_frozen" ISNULL
2329 UNION ALL
2330 SELECT
2331 "accepted" AS "occurrence",
2332 'issue_accepted'::"timeline_event" AS "event",
2333 "id" AS "issue_id"
2334 FROM "issue" WHERE "accepted" NOTNULL
2335 UNION ALL
2336 SELECT
2337 "half_frozen" AS "occurrence",
2338 'issue_half_frozen'::"timeline_event" AS "event",
2339 "id" AS "issue_id"
2340 FROM "issue" WHERE "half_frozen" NOTNULL
2341 UNION ALL
2342 SELECT
2343 "fully_frozen" AS "occurrence",
2344 'issue_voting_started'::"timeline_event" AS "event",
2345 "id" AS "issue_id"
2346 FROM "issue"
2347 WHERE "fully_frozen" NOTNULL
2348 AND ("closed" ISNULL OR "closed" != "fully_frozen")
2349 UNION ALL
2350 SELECT
2351 "closed" AS "occurrence",
2352 CASE WHEN "fully_frozen" = "closed" THEN
2353 'issue_finished_without_voting'::"timeline_event"
2354 ELSE
2355 'issue_finished_after_voting'::"timeline_event"
2356 END AS "event",
2357 "id" AS "issue_id"
2358 FROM "issue" WHERE "closed" NOTNULL AND "fully_frozen" NOTNULL;
2360 COMMENT ON VIEW "timeline_issue" IS 'Helper view for "timeline" view (DEPRECATED)';
2363 CREATE VIEW "timeline_initiative" AS
2364 SELECT
2365 "created" AS "occurrence",
2366 'initiative_created'::"timeline_event" AS "event",
2367 "id" AS "initiative_id"
2368 FROM "initiative"
2369 UNION ALL
2370 SELECT
2371 "revoked" AS "occurrence",
2372 'initiative_revoked'::"timeline_event" AS "event",
2373 "id" AS "initiative_id"
2374 FROM "initiative" WHERE "revoked" NOTNULL;
2376 COMMENT ON VIEW "timeline_initiative" IS 'Helper view for "timeline" view (DEPRECATED)';
2379 CREATE VIEW "timeline_draft" AS
2380 SELECT
2381 "created" AS "occurrence",
2382 'draft_created'::"timeline_event" AS "event",
2383 "id" AS "draft_id"
2384 FROM "draft";
2386 COMMENT ON VIEW "timeline_draft" IS 'Helper view for "timeline" view (DEPRECATED)';
2389 CREATE VIEW "timeline_suggestion" AS
2390 SELECT
2391 "created" AS "occurrence",
2392 'suggestion_created'::"timeline_event" AS "event",
2393 "id" AS "suggestion_id"
2394 FROM "suggestion";
2396 COMMENT ON VIEW "timeline_suggestion" IS 'Helper view for "timeline" view (DEPRECATED)';
2399 CREATE VIEW "timeline" AS
2400 SELECT
2401 "occurrence",
2402 "event",
2403 "issue_id",
2404 NULL AS "initiative_id",
2405 NULL::INT8 AS "draft_id", -- TODO: Why do we need a type-cast here? Is this due to 32 bit architecture?
2406 NULL::INT8 AS "suggestion_id"
2407 FROM "timeline_issue"
2408 UNION ALL
2409 SELECT
2410 "occurrence",
2411 "event",
2412 NULL AS "issue_id",
2413 "initiative_id",
2414 NULL AS "draft_id",
2415 NULL AS "suggestion_id"
2416 FROM "timeline_initiative"
2417 UNION ALL
2418 SELECT
2419 "occurrence",
2420 "event",
2421 NULL AS "issue_id",
2422 NULL AS "initiative_id",
2423 "draft_id",
2424 NULL AS "suggestion_id"
2425 FROM "timeline_draft"
2426 UNION ALL
2427 SELECT
2428 "occurrence",
2429 "event",
2430 NULL AS "issue_id",
2431 NULL AS "initiative_id",
2432 NULL AS "draft_id",
2433 "suggestion_id"
2434 FROM "timeline_suggestion";
2436 COMMENT ON VIEW "timeline" IS 'Aggregation of different events in the system (DEPRECATED)';
2440 ------------------------------------------------------
2441 -- Row set returning function for delegation chains --
2442 ------------------------------------------------------
2445 CREATE TYPE "delegation_chain_loop_tag" AS ENUM
2446 ('first', 'intermediate', 'last', 'repetition');
2448 COMMENT ON TYPE "delegation_chain_loop_tag" IS 'Type for loop tags in "delegation_chain_row" type';
2451 CREATE TYPE "delegation_chain_row" AS (
2452 "index" INT4,
2453 "member_id" INT4,
2454 "member_valid" BOOLEAN,
2455 "participation" BOOLEAN,
2456 "overridden" BOOLEAN,
2457 "scope_in" "delegation_scope",
2458 "scope_out" "delegation_scope",
2459 "disabled_out" BOOLEAN,
2460 "loop" "delegation_chain_loop_tag" );
2462 COMMENT ON TYPE "delegation_chain_row" IS 'Type of rows returned by "delegation_chain" function';
2464 COMMENT ON COLUMN "delegation_chain_row"."index" IS 'Index starting with 0 and counting up';
2465 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';
2466 COMMENT ON COLUMN "delegation_chain_row"."overridden" IS 'True, if an entry with lower index has "participation" set to true';
2467 COMMENT ON COLUMN "delegation_chain_row"."scope_in" IS 'Scope of used incoming delegation';
2468 COMMENT ON COLUMN "delegation_chain_row"."scope_out" IS 'Scope of used outgoing delegation';
2469 COMMENT ON COLUMN "delegation_chain_row"."disabled_out" IS 'Outgoing delegation is explicitly disabled by a delegation with trustee_id set to NULL';
2470 COMMENT ON COLUMN "delegation_chain_row"."loop" IS 'Not null, if member is part of a loop, see "delegation_chain_loop_tag" type';
2473 CREATE FUNCTION "delegation_chain_for_closed_issue"
2474 ( "member_id_p" "member"."id"%TYPE,
2475 "issue_id_p" "issue"."id"%TYPE )
2476 RETURNS SETOF "delegation_chain_row"
2477 LANGUAGE 'plpgsql' STABLE AS $$
2478 DECLARE
2479 "output_row" "delegation_chain_row";
2480 "direct_voter_row" "direct_voter"%ROWTYPE;
2481 "delegating_voter_row" "delegating_voter"%ROWTYPE;
2482 BEGIN
2483 "output_row"."index" := 0;
2484 "output_row"."member_id" := "member_id_p";
2485 "output_row"."member_valid" := TRUE;
2486 "output_row"."participation" := FALSE;
2487 "output_row"."overridden" := FALSE;
2488 "output_row"."disabled_out" := FALSE;
2489 LOOP
2490 SELECT INTO "direct_voter_row" * FROM "direct_voter"
2491 WHERE "issue_id" = "issue_id_p"
2492 AND "member_id" = "output_row"."member_id";
2493 IF "direct_voter_row"."member_id" NOTNULL THEN
2494 "output_row"."participation" := TRUE;
2495 "output_row"."scope_out" := NULL;
2496 "output_row"."disabled_out" := NULL;
2497 RETURN NEXT "output_row";
2498 RETURN;
2499 END IF;
2500 SELECT INTO "delegating_voter_row" * FROM "delegating_voter"
2501 WHERE "issue_id" = "issue_id_p"
2502 AND "member_id" = "output_row"."member_id";
2503 IF "delegating_voter_row"."member_id" ISNULL THEN
2504 RETURN;
2505 END IF;
2506 "output_row"."scope_out" := "delegating_voter_row"."scope";
2507 RETURN NEXT "output_row";
2508 "output_row"."member_id" := "delegating_voter_row"."delegate_member_ids"[1];
2509 "output_row"."scope_in" := "output_row"."scope_out";
2510 END LOOP;
2511 END;
2512 $$;
2514 COMMENT ON FUNCTION "delegation_chain_for_closed_issue"
2515 ( "member"."id"%TYPE,
2516 "member"."id"%TYPE )
2517 IS 'Helper function for "delegation_chain" function, handling the special case of closed issues after voting';
2520 CREATE FUNCTION "delegation_chain"
2521 ( "member_id_p" "member"."id"%TYPE,
2522 "unit_id_p" "unit"."id"%TYPE,
2523 "area_id_p" "area"."id"%TYPE,
2524 "issue_id_p" "issue"."id"%TYPE,
2525 "simulate_trustee_id_p" "member"."id"%TYPE DEFAULT NULL,
2526 "simulate_default_p" BOOLEAN DEFAULT FALSE )
2527 RETURNS SETOF "delegation_chain_row"
2528 LANGUAGE 'plpgsql' STABLE AS $$
2529 DECLARE
2530 "scope_v" "delegation_scope";
2531 "unit_id_v" "unit"."id"%TYPE;
2532 "area_id_v" "area"."id"%TYPE;
2533 "issue_row" "issue"%ROWTYPE;
2534 "visited_member_ids" INT4[]; -- "member"."id"%TYPE[]
2535 "loop_member_id_v" "member"."id"%TYPE;
2536 "output_row" "delegation_chain_row";
2537 "output_rows" "delegation_chain_row"[];
2538 "simulate_v" BOOLEAN;
2539 "simulate_here_v" BOOLEAN;
2540 "delegation_row" "delegation"%ROWTYPE;
2541 "row_count" INT4;
2542 "i" INT4;
2543 "loop_v" BOOLEAN;
2544 BEGIN
2545 IF "simulate_trustee_id_p" NOTNULL AND "simulate_default_p" THEN
2546 RAISE EXCEPTION 'Both "simulate_trustee_id_p" is set, and "simulate_default_p" is true';
2547 END IF;
2548 IF "simulate_trustee_id_p" NOTNULL OR "simulate_default_p" THEN
2549 "simulate_v" := TRUE;
2550 ELSE
2551 "simulate_v" := FALSE;
2552 END IF;
2553 IF
2554 "unit_id_p" NOTNULL AND
2555 "area_id_p" ISNULL AND
2556 "issue_id_p" ISNULL
2557 THEN
2558 "scope_v" := 'unit';
2559 "unit_id_v" := "unit_id_p";
2560 ELSIF
2561 "unit_id_p" ISNULL AND
2562 "area_id_p" NOTNULL AND
2563 "issue_id_p" ISNULL
2564 THEN
2565 "scope_v" := 'area';
2566 "area_id_v" := "area_id_p";
2567 SELECT "unit_id" INTO "unit_id_v"
2568 FROM "area" WHERE "id" = "area_id_v";
2569 ELSIF
2570 "unit_id_p" ISNULL AND
2571 "area_id_p" ISNULL AND
2572 "issue_id_p" NOTNULL
2573 THEN
2574 SELECT INTO "issue_row" * FROM "issue" WHERE "id" = "issue_id_p";
2575 IF "issue_row"."id" ISNULL THEN
2576 RETURN;
2577 END IF;
2578 IF "issue_row"."closed" NOTNULL THEN
2579 IF "simulate_v" THEN
2580 RAISE EXCEPTION 'Tried to simulate delegation chain for closed issue.';
2581 END IF;
2582 FOR "output_row" IN
2583 SELECT * FROM
2584 "delegation_chain_for_closed_issue"("member_id_p", "issue_id_p")
2585 LOOP
2586 RETURN NEXT "output_row";
2587 END LOOP;
2588 RETURN;
2589 END IF;
2590 "scope_v" := 'issue';
2591 SELECT "area_id" INTO "area_id_v"
2592 FROM "issue" WHERE "id" = "issue_id_p";
2593 SELECT "unit_id" INTO "unit_id_v"
2594 FROM "area" WHERE "id" = "area_id_v";
2595 ELSE
2596 RAISE EXCEPTION 'Exactly one of unit_id_p, area_id_p, or issue_id_p must be NOTNULL.';
2597 END IF;
2598 "visited_member_ids" := '{}';
2599 "loop_member_id_v" := NULL;
2600 "output_rows" := '{}';
2601 "output_row"."index" := 0;
2602 "output_row"."member_id" := "member_id_p";
2603 "output_row"."member_valid" := TRUE;
2604 "output_row"."participation" := FALSE;
2605 "output_row"."overridden" := FALSE;
2606 "output_row"."disabled_out" := FALSE;
2607 "output_row"."scope_out" := NULL;
2608 LOOP
2609 IF "visited_member_ids" @> ARRAY["output_row"."member_id"] THEN
2610 "loop_member_id_v" := "output_row"."member_id";
2611 ELSE
2612 "visited_member_ids" :=
2613 "visited_member_ids" || "output_row"."member_id";
2614 END IF;
2615 IF "output_row"."participation" ISNULL THEN
2616 "output_row"."overridden" := NULL;
2617 ELSIF "output_row"."participation" THEN
2618 "output_row"."overridden" := TRUE;
2619 END IF;
2620 "output_row"."scope_in" := "output_row"."scope_out";
2621 "output_row"."member_valid" := EXISTS (
2622 SELECT NULL FROM "member" JOIN "privilege"
2623 ON "privilege"."member_id" = "member"."id"
2624 AND "privilege"."unit_id" = "unit_id_v"
2625 WHERE "id" = "output_row"."member_id"
2626 AND "member"."active" AND "privilege"."voting_right"
2627 );
2628 "simulate_here_v" := (
2629 "simulate_v" AND
2630 "output_row"."member_id" = "member_id_p"
2631 );
2632 "delegation_row" := ROW(NULL);
2633 IF "output_row"."member_valid" OR "simulate_here_v" THEN
2634 IF "scope_v" = 'unit' THEN
2635 IF NOT "simulate_here_v" THEN
2636 SELECT * INTO "delegation_row" FROM "delegation"
2637 WHERE "truster_id" = "output_row"."member_id"
2638 AND "unit_id" = "unit_id_v";
2639 END IF;
2640 ELSIF "scope_v" = 'area' THEN
2641 "output_row"."participation" := EXISTS (
2642 SELECT NULL FROM "membership"
2643 WHERE "area_id" = "area_id_p"
2644 AND "member_id" = "output_row"."member_id"
2645 );
2646 IF "simulate_here_v" THEN
2647 IF "simulate_trustee_id_p" ISNULL THEN
2648 SELECT * INTO "delegation_row" FROM "delegation"
2649 WHERE "truster_id" = "output_row"."member_id"
2650 AND "unit_id" = "unit_id_v";
2651 END IF;
2652 ELSE
2653 SELECT * INTO "delegation_row" FROM "delegation"
2654 WHERE "truster_id" = "output_row"."member_id"
2655 AND (
2656 "unit_id" = "unit_id_v" OR
2657 "area_id" = "area_id_v"
2658 )
2659 ORDER BY "scope" DESC;
2660 END IF;
2661 ELSIF "scope_v" = 'issue' THEN
2662 IF "issue_row"."fully_frozen" ISNULL THEN
2663 "output_row"."participation" := EXISTS (
2664 SELECT NULL FROM "interest"
2665 WHERE "issue_id" = "issue_id_p"
2666 AND "member_id" = "output_row"."member_id"
2667 );
2668 ELSE
2669 IF "output_row"."member_id" = "member_id_p" THEN
2670 "output_row"."participation" := EXISTS (
2671 SELECT NULL FROM "direct_voter"
2672 WHERE "issue_id" = "issue_id_p"
2673 AND "member_id" = "output_row"."member_id"
2674 );
2675 ELSE
2676 "output_row"."participation" := NULL;
2677 END IF;
2678 END IF;
2679 IF "simulate_here_v" THEN
2680 IF "simulate_trustee_id_p" ISNULL THEN
2681 SELECT * INTO "delegation_row" FROM "delegation"
2682 WHERE "truster_id" = "output_row"."member_id"
2683 AND (
2684 "unit_id" = "unit_id_v" OR
2685 "area_id" = "area_id_v"
2686 )
2687 ORDER BY "scope" DESC;
2688 END IF;
2689 ELSE
2690 SELECT * INTO "delegation_row" FROM "delegation"
2691 WHERE "truster_id" = "output_row"."member_id"
2692 AND (
2693 "unit_id" = "unit_id_v" OR
2694 "area_id" = "area_id_v" OR
2695 "issue_id" = "issue_id_p"
2696 )
2697 ORDER BY "scope" DESC;
2698 END IF;
2699 END IF;
2700 ELSE
2701 "output_row"."participation" := FALSE;
2702 END IF;
2703 IF "simulate_here_v" AND "simulate_trustee_id_p" NOTNULL THEN
2704 "output_row"."scope_out" := "scope_v";
2705 "output_rows" := "output_rows" || "output_row";
2706 "output_row"."member_id" := "simulate_trustee_id_p";
2707 ELSIF "delegation_row"."trustee_id" NOTNULL THEN
2708 "output_row"."scope_out" := "delegation_row"."scope";
2709 "output_rows" := "output_rows" || "output_row";
2710 "output_row"."member_id" := "delegation_row"."trustee_id";
2711 ELSIF "delegation_row"."scope" NOTNULL THEN
2712 "output_row"."scope_out" := "delegation_row"."scope";
2713 "output_row"."disabled_out" := TRUE;
2714 "output_rows" := "output_rows" || "output_row";
2715 EXIT;
2716 ELSE
2717 "output_row"."scope_out" := NULL;
2718 "output_rows" := "output_rows" || "output_row";
2719 EXIT;
2720 END IF;
2721 EXIT WHEN "loop_member_id_v" NOTNULL;
2722 "output_row"."index" := "output_row"."index" + 1;
2723 END LOOP;
2724 "row_count" := array_upper("output_rows", 1);
2725 "i" := 1;
2726 "loop_v" := FALSE;
2727 LOOP
2728 "output_row" := "output_rows"["i"];
2729 EXIT WHEN "output_row" ISNULL; -- NOTE: ISNULL and NOT ... NOTNULL produce different results!
2730 IF "loop_v" THEN
2731 IF "i" + 1 = "row_count" THEN
2732 "output_row"."loop" := 'last';
2733 ELSIF "i" = "row_count" THEN
2734 "output_row"."loop" := 'repetition';
2735 ELSE
2736 "output_row"."loop" := 'intermediate';
2737 END IF;
2738 ELSIF "output_row"."member_id" = "loop_member_id_v" THEN
2739 "output_row"."loop" := 'first';
2740 "loop_v" := TRUE;
2741 END IF;
2742 IF "scope_v" = 'unit' THEN
2743 "output_row"."participation" := NULL;
2744 END IF;
2745 RETURN NEXT "output_row";
2746 "i" := "i" + 1;
2747 END LOOP;
2748 RETURN;
2749 END;
2750 $$;
2752 COMMENT ON FUNCTION "delegation_chain"
2753 ( "member"."id"%TYPE,
2754 "unit"."id"%TYPE,
2755 "area"."id"%TYPE,
2756 "issue"."id"%TYPE,
2757 "member"."id"%TYPE,
2758 BOOLEAN )
2759 IS 'Shows a delegation chain for unit, area, or issue; See "delegation_chain_row" type for more information';
2763 ---------------------------------------------------------
2764 -- Single row returning function for delegation chains --
2765 ---------------------------------------------------------
2768 CREATE TYPE "delegation_info_loop_type" AS ENUM
2769 ('own', 'first', 'first_ellipsis', 'other', 'other_ellipsis');
2771 COMMENT ON TYPE "delegation_info_loop_type" IS 'Type of "delegation_loop" in "delegation_info_type"; ''own'' means loop to self, ''first'' means loop to first trustee, ''first_ellipsis'' means loop to ellipsis after first trustee, ''other'' means loop to other trustee, ''other_ellipsis'' means loop to ellipsis after other trustee''';
2774 CREATE TYPE "delegation_info_type" AS (
2775 "own_participation" BOOLEAN,
2776 "own_delegation_scope" "delegation_scope",
2777 "first_trustee_id" INT4,
2778 "first_trustee_participation" BOOLEAN,
2779 "first_trustee_ellipsis" BOOLEAN,
2780 "other_trustee_id" INT4,
2781 "other_trustee_participation" BOOLEAN,
2782 "other_trustee_ellipsis" BOOLEAN,
2783 "delegation_loop" "delegation_info_loop_type",
2784 "participating_member_id" INT4 );
2786 COMMENT ON TYPE "delegation_info_type" IS 'Type of result returned by "delegation_info" function; For meaning of "participation" check comment on "delegation_chain_row" type';
2788 COMMENT ON COLUMN "delegation_info_type"."own_participation" IS 'Member is directly participating';
2789 COMMENT ON COLUMN "delegation_info_type"."own_delegation_scope" IS 'Delegation scope of member';
2790 COMMENT ON COLUMN "delegation_info_type"."first_trustee_id" IS 'Direct trustee of member';
2791 COMMENT ON COLUMN "delegation_info_type"."first_trustee_participation" IS 'Direct trustee of member is participating';
2792 COMMENT ON COLUMN "delegation_info_type"."first_trustee_ellipsis" IS 'Ellipsis in delegation chain after "first_trustee"';
2793 COMMENT ON COLUMN "delegation_info_type"."other_trustee_id" IS 'Another relevant trustee (due to participation)';
2794 COMMENT ON COLUMN "delegation_info_type"."other_trustee_participation" IS 'Another trustee is participating (redundant field: if "other_trustee_id" is set, then "other_trustee_participation" is always TRUE, else "other_trustee_participation" is NULL)';
2795 COMMENT ON COLUMN "delegation_info_type"."other_trustee_ellipsis" IS 'Ellipsis in delegation chain after "other_trustee"';
2796 COMMENT ON COLUMN "delegation_info_type"."delegation_loop" IS 'Non-NULL value, if delegation chain contains a circle; See comment on "delegation_info_loop_type" for details';
2797 COMMENT ON COLUMN "delegation_info_type"."participating_member_id" IS 'First participating member in delegation chain';
2800 CREATE FUNCTION "delegation_info"
2801 ( "member_id_p" "member"."id"%TYPE,
2802 "unit_id_p" "unit"."id"%TYPE,
2803 "area_id_p" "area"."id"%TYPE,
2804 "issue_id_p" "issue"."id"%TYPE,
2805 "simulate_trustee_id_p" "member"."id"%TYPE DEFAULT NULL,
2806 "simulate_default_p" BOOLEAN DEFAULT FALSE )
2807 RETURNS "delegation_info_type"
2808 LANGUAGE 'plpgsql' STABLE AS $$
2809 DECLARE
2810 "current_row" "delegation_chain_row";
2811 "result" "delegation_info_type";
2812 BEGIN
2813 "result"."own_participation" := FALSE;
2814 FOR "current_row" IN
2815 SELECT * FROM "delegation_chain"(
2816 "member_id_p",
2817 "unit_id_p", "area_id_p", "issue_id_p",
2818 "simulate_trustee_id_p", "simulate_default_p")
2819 LOOP
2820 IF
2821 "result"."participating_member_id" ISNULL AND
2822 "current_row"."participation"
2823 THEN
2824 "result"."participating_member_id" := "current_row"."member_id";
2825 END IF;
2826 IF "current_row"."member_id" = "member_id_p" THEN
2827 "result"."own_participation" := "current_row"."participation";
2828 "result"."own_delegation_scope" := "current_row"."scope_out";
2829 IF "current_row"."loop" = 'first' THEN
2830 "result"."delegation_loop" := 'own';
2831 END IF;
2832 ELSIF
2833 "current_row"."member_valid" AND
2834 ( "current_row"."loop" ISNULL OR
2835 "current_row"."loop" != 'repetition' )
2836 THEN
2837 IF "result"."first_trustee_id" ISNULL THEN
2838 "result"."first_trustee_id" := "current_row"."member_id";
2839 "result"."first_trustee_participation" := "current_row"."participation";
2840 "result"."first_trustee_ellipsis" := FALSE;
2841 IF "current_row"."loop" = 'first' THEN
2842 "result"."delegation_loop" := 'first';
2843 END IF;
2844 ELSIF "result"."other_trustee_id" ISNULL THEN
2845 IF "current_row"."participation" AND NOT "current_row"."overridden" THEN
2846 "result"."other_trustee_id" := "current_row"."member_id";
2847 "result"."other_trustee_participation" := TRUE;
2848 "result"."other_trustee_ellipsis" := FALSE;
2849 IF "current_row"."loop" = 'first' THEN
2850 "result"."delegation_loop" := 'other';
2851 END IF;
2852 ELSE
2853 "result"."first_trustee_ellipsis" := TRUE;
2854 IF "current_row"."loop" = 'first' THEN
2855 "result"."delegation_loop" := 'first_ellipsis';
2856 END IF;
2857 END IF;
2858 ELSE
2859 "result"."other_trustee_ellipsis" := TRUE;
2860 IF "current_row"."loop" = 'first' THEN
2861 "result"."delegation_loop" := 'other_ellipsis';
2862 END IF;
2863 END IF;
2864 END IF;
2865 END LOOP;
2866 RETURN "result";
2867 END;
2868 $$;
2870 COMMENT ON FUNCTION "delegation_info"
2871 ( "member"."id"%TYPE,
2872 "unit"."id"%TYPE,
2873 "area"."id"%TYPE,
2874 "issue"."id"%TYPE,
2875 "member"."id"%TYPE,
2876 BOOLEAN )
2877 IS 'Notable information about a delegation chain for unit, area, or issue; See "delegation_info_type" for more information';
2881 ------------------------------
2882 -- Comparison by vote count --
2883 ------------------------------
2885 CREATE FUNCTION "vote_ratio"
2886 ( "positive_votes_p" "initiative"."positive_votes"%TYPE,
2887 "negative_votes_p" "initiative"."negative_votes"%TYPE )
2888 RETURNS FLOAT8
2889 LANGUAGE 'plpgsql' STABLE AS $$
2890 BEGIN
2891 IF "positive_votes_p" > 0 AND "negative_votes_p" > 0 THEN
2892 RETURN
2893 "positive_votes_p"::FLOAT8 /
2894 ("positive_votes_p" + "negative_votes_p")::FLOAT8;
2895 ELSIF "positive_votes_p" > 0 THEN
2896 RETURN "positive_votes_p";
2897 ELSIF "negative_votes_p" > 0 THEN
2898 RETURN 1 - "negative_votes_p";
2899 ELSE
2900 RETURN 0.5;
2901 END IF;
2902 END;
2903 $$;
2905 COMMENT ON FUNCTION "vote_ratio"
2906 ( "initiative"."positive_votes"%TYPE,
2907 "initiative"."negative_votes"%TYPE )
2908 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.';
2912 ------------------------------------------------
2913 -- Locking for snapshots and voting procedure --
2914 ------------------------------------------------
2917 CREATE FUNCTION "share_row_lock_issue_trigger"()
2918 RETURNS TRIGGER
2919 LANGUAGE 'plpgsql' VOLATILE AS $$
2920 BEGIN
2921 IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
2922 PERFORM NULL FROM "issue" WHERE "id" = OLD."issue_id" FOR SHARE;
2923 END IF;
2924 IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
2925 PERFORM NULL FROM "issue" WHERE "id" = NEW."issue_id" FOR SHARE;
2926 RETURN NEW;
2927 ELSE
2928 RETURN OLD;
2929 END IF;
2930 END;
2931 $$;
2933 COMMENT ON FUNCTION "share_row_lock_issue_trigger"() IS 'Implementation of triggers "share_row_lock_issue" on multiple tables';
2936 CREATE FUNCTION "share_row_lock_issue_via_initiative_trigger"()
2937 RETURNS TRIGGER
2938 LANGUAGE 'plpgsql' VOLATILE AS $$
2939 BEGIN
2940 IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
2941 PERFORM NULL FROM "issue"
2942 JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
2943 WHERE "initiative"."id" = OLD."initiative_id"
2944 FOR SHARE OF "issue";
2945 END IF;
2946 IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
2947 PERFORM NULL FROM "issue"
2948 JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
2949 WHERE "initiative"."id" = NEW."initiative_id"
2950 FOR SHARE OF "issue";
2951 RETURN NEW;
2952 ELSE
2953 RETURN OLD;
2954 END IF;
2955 END;
2956 $$;
2958 COMMENT ON FUNCTION "share_row_lock_issue_trigger"() IS 'Implementation of trigger "share_row_lock_issue_via_initiative" on table "opinion"';
2961 CREATE TRIGGER "share_row_lock_issue"
2962 BEFORE INSERT OR UPDATE OR DELETE ON "initiative"
2963 FOR EACH ROW EXECUTE PROCEDURE
2964 "share_row_lock_issue_trigger"();
2966 CREATE TRIGGER "share_row_lock_issue"
2967 BEFORE INSERT OR UPDATE OR DELETE ON "interest"
2968 FOR EACH ROW EXECUTE PROCEDURE
2969 "share_row_lock_issue_trigger"();
2971 CREATE TRIGGER "share_row_lock_issue"
2972 BEFORE INSERT OR UPDATE OR DELETE ON "supporter"
2973 FOR EACH ROW EXECUTE PROCEDURE
2974 "share_row_lock_issue_trigger"();
2976 CREATE TRIGGER "share_row_lock_issue_via_initiative"
2977 BEFORE INSERT OR UPDATE OR DELETE ON "opinion"
2978 FOR EACH ROW EXECUTE PROCEDURE
2979 "share_row_lock_issue_via_initiative_trigger"();
2981 CREATE TRIGGER "share_row_lock_issue"
2982 BEFORE INSERT OR UPDATE OR DELETE ON "direct_voter"
2983 FOR EACH ROW EXECUTE PROCEDURE
2984 "share_row_lock_issue_trigger"();
2986 CREATE TRIGGER "share_row_lock_issue"
2987 BEFORE INSERT OR UPDATE OR DELETE ON "delegating_voter"
2988 FOR EACH ROW EXECUTE PROCEDURE
2989 "share_row_lock_issue_trigger"();
2991 CREATE TRIGGER "share_row_lock_issue"
2992 BEFORE INSERT OR UPDATE OR DELETE ON "vote"
2993 FOR EACH ROW EXECUTE PROCEDURE
2994 "share_row_lock_issue_trigger"();
2996 COMMENT ON TRIGGER "share_row_lock_issue" ON "initiative" IS 'See "lock_issue" function';
2997 COMMENT ON TRIGGER "share_row_lock_issue" ON "interest" IS 'See "lock_issue" function';
2998 COMMENT ON TRIGGER "share_row_lock_issue" ON "supporter" IS 'See "lock_issue" function';
2999 COMMENT ON TRIGGER "share_row_lock_issue_via_initiative" ON "opinion" IS 'See "lock_issue" function';
3000 COMMENT ON TRIGGER "share_row_lock_issue" ON "direct_voter" IS 'See "lock_issue" function';
3001 COMMENT ON TRIGGER "share_row_lock_issue" ON "delegating_voter" IS 'See "lock_issue" function';
3002 COMMENT ON TRIGGER "share_row_lock_issue" ON "vote" IS 'See "lock_issue" function';
3005 CREATE FUNCTION "lock_issue"
3006 ( "issue_id_p" "issue"."id"%TYPE )
3007 RETURNS VOID
3008 LANGUAGE 'plpgsql' VOLATILE AS $$
3009 BEGIN
3010 LOCK TABLE "member" IN SHARE MODE;
3011 LOCK TABLE "privilege" IN SHARE MODE;
3012 LOCK TABLE "membership" IN SHARE MODE;
3013 LOCK TABLE "policy" IN SHARE MODE;
3014 PERFORM NULL FROM "issue" WHERE "id" = "issue_id_p" FOR UPDATE;
3015 -- NOTE: The row-level exclusive lock in combination with the
3016 -- share_row_lock_issue(_via_initiative)_trigger functions (which
3017 -- acquire a row-level share lock on the issue) ensure that no data
3018 -- is changed, which could affect calculation of snapshots or
3019 -- counting of votes. Table "delegation" must be table-level-locked,
3020 -- as it also contains issue- and global-scope delegations.
3021 LOCK TABLE "delegation" IN SHARE MODE;
3022 LOCK TABLE "direct_population_snapshot" IN EXCLUSIVE MODE;
3023 LOCK TABLE "delegating_population_snapshot" IN EXCLUSIVE MODE;
3024 LOCK TABLE "direct_interest_snapshot" IN EXCLUSIVE MODE;
3025 LOCK TABLE "delegating_interest_snapshot" IN EXCLUSIVE MODE;
3026 LOCK TABLE "direct_supporter_snapshot" IN EXCLUSIVE MODE;
3027 RETURN;
3028 END;
3029 $$;
3031 COMMENT ON FUNCTION "lock_issue"
3032 ( "issue"."id"%TYPE )
3033 IS 'Locks the issue and all other data which is used for calculating snapshots or counting votes.';
3037 ------------------------------------------------------------------------
3038 -- Regular tasks, except calculcation of snapshots and voting results --
3039 ------------------------------------------------------------------------
3041 CREATE FUNCTION "check_activity"()
3042 RETURNS VOID
3043 LANGUAGE 'plpgsql' VOLATILE AS $$
3044 DECLARE
3045 "system_setting_row" "system_setting"%ROWTYPE;
3046 BEGIN
3047 SELECT * INTO "system_setting_row" FROM "system_setting";
3048 LOCK TABLE "member" IN SHARE ROW EXCLUSIVE MODE;
3049 IF "system_setting_row"."member_ttl" NOTNULL THEN
3050 UPDATE "member" SET "active" = FALSE
3051 WHERE "active" = TRUE
3052 AND "last_activity" < (now() - "system_setting_row"."member_ttl")::DATE;
3053 END IF;
3054 RETURN;
3055 END;
3056 $$;
3058 COMMENT ON FUNCTION "check_activity"() IS 'Deactivates members when "last_activity" is older than "system_setting"."member_ttl".';
3061 CREATE FUNCTION "calculate_member_counts"()
3062 RETURNS VOID
3063 LANGUAGE 'plpgsql' VOLATILE AS $$
3064 BEGIN
3065 LOCK TABLE "member" IN SHARE MODE;
3066 LOCK TABLE "member_count" IN EXCLUSIVE MODE;
3067 LOCK TABLE "unit" IN EXCLUSIVE MODE;
3068 LOCK TABLE "area" IN EXCLUSIVE MODE;
3069 LOCK TABLE "privilege" IN SHARE MODE;
3070 LOCK TABLE "membership" IN SHARE MODE;
3071 DELETE FROM "member_count";
3072 INSERT INTO "member_count" ("total_count")
3073 SELECT "total_count" FROM "member_count_view";
3074 UPDATE "unit" SET "member_count" = "view"."member_count"
3075 FROM "unit_member_count" AS "view"
3076 WHERE "view"."unit_id" = "unit"."id";
3077 UPDATE "area" SET
3078 "direct_member_count" = "view"."direct_member_count",
3079 "member_weight" = "view"."member_weight"
3080 FROM "area_member_count" AS "view"
3081 WHERE "view"."area_id" = "area"."id";
3082 RETURN;
3083 END;
3084 $$;
3086 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"';
3090 ------------------------------
3091 -- Calculation of snapshots --
3092 ------------------------------
3094 CREATE FUNCTION "weight_of_added_delegations_for_population_snapshot"
3095 ( "issue_id_p" "issue"."id"%TYPE,
3096 "member_id_p" "member"."id"%TYPE,
3097 "delegate_member_ids_p" "delegating_population_snapshot"."delegate_member_ids"%TYPE )
3098 RETURNS "direct_population_snapshot"."weight"%TYPE
3099 LANGUAGE 'plpgsql' VOLATILE AS $$
3100 DECLARE
3101 "issue_delegation_row" "issue_delegation"%ROWTYPE;
3102 "delegate_member_ids_v" "delegating_population_snapshot"."delegate_member_ids"%TYPE;
3103 "weight_v" INT4;
3104 "sub_weight_v" INT4;
3105 BEGIN
3106 "weight_v" := 0;
3107 FOR "issue_delegation_row" IN
3108 SELECT * FROM "issue_delegation"
3109 WHERE "trustee_id" = "member_id_p"
3110 AND "issue_id" = "issue_id_p"
3111 LOOP
3112 IF NOT EXISTS (
3113 SELECT NULL FROM "direct_population_snapshot"
3114 WHERE "issue_id" = "issue_id_p"
3115 AND "event" = 'periodic'
3116 AND "member_id" = "issue_delegation_row"."truster_id"
3117 ) AND NOT EXISTS (
3118 SELECT NULL FROM "delegating_population_snapshot"
3119 WHERE "issue_id" = "issue_id_p"
3120 AND "event" = 'periodic'
3121 AND "member_id" = "issue_delegation_row"."truster_id"
3122 ) THEN
3123 "delegate_member_ids_v" :=
3124 "member_id_p" || "delegate_member_ids_p";
3125 INSERT INTO "delegating_population_snapshot" (
3126 "issue_id",
3127 "event",
3128 "member_id",
3129 "scope",
3130 "delegate_member_ids"
3131 ) VALUES (
3132 "issue_id_p",
3133 'periodic',
3134 "issue_delegation_row"."truster_id",
3135 "issue_delegation_row"."scope",
3136 "delegate_member_ids_v"
3137 );
3138 "sub_weight_v" := 1 +
3139 "weight_of_added_delegations_for_population_snapshot"(
3140 "issue_id_p",
3141 "issue_delegation_row"."truster_id",
3142 "delegate_member_ids_v"
3143 );
3144 UPDATE "delegating_population_snapshot"
3145 SET "weight" = "sub_weight_v"
3146 WHERE "issue_id" = "issue_id_p"
3147 AND "event" = 'periodic'
3148 AND "member_id" = "issue_delegation_row"."truster_id";
3149 "weight_v" := "weight_v" + "sub_weight_v";
3150 END IF;
3151 END LOOP;
3152 RETURN "weight_v";
3153 END;
3154 $$;
3156 COMMENT ON FUNCTION "weight_of_added_delegations_for_population_snapshot"
3157 ( "issue"."id"%TYPE,
3158 "member"."id"%TYPE,
3159 "delegating_population_snapshot"."delegate_member_ids"%TYPE )
3160 IS 'Helper function for "create_population_snapshot" function';
3163 CREATE FUNCTION "create_population_snapshot"
3164 ( "issue_id_p" "issue"."id"%TYPE )
3165 RETURNS VOID
3166 LANGUAGE 'plpgsql' VOLATILE AS $$
3167 DECLARE
3168 "member_id_v" "member"."id"%TYPE;
3169 BEGIN
3170 DELETE FROM "direct_population_snapshot"
3171 WHERE "issue_id" = "issue_id_p"
3172 AND "event" = 'periodic';
3173 DELETE FROM "delegating_population_snapshot"
3174 WHERE "issue_id" = "issue_id_p"
3175 AND "event" = 'periodic';
3176 INSERT INTO "direct_population_snapshot"
3177 ("issue_id", "event", "member_id")
3178 SELECT
3179 "issue_id_p" AS "issue_id",
3180 'periodic'::"snapshot_event" AS "event",
3181 "member"."id" AS "member_id"
3182 FROM "issue"
3183 JOIN "area" ON "issue"."area_id" = "area"."id"
3184 JOIN "membership" ON "area"."id" = "membership"."area_id"
3185 JOIN "member" ON "membership"."member_id" = "member"."id"
3186 JOIN "privilege"
3187 ON "privilege"."unit_id" = "area"."unit_id"
3188 AND "privilege"."member_id" = "member"."id"
3189 WHERE "issue"."id" = "issue_id_p"
3190 AND "member"."active" AND "privilege"."voting_right"
3191 UNION
3192 SELECT
3193 "issue_id_p" AS "issue_id",
3194 'periodic'::"snapshot_event" AS "event",
3195 "member"."id" AS "member_id"
3196 FROM "issue"
3197 JOIN "area" ON "issue"."area_id" = "area"."id"
3198 JOIN "interest" ON "issue"."id" = "interest"."issue_id"
3199 JOIN "member" ON "interest"."member_id" = "member"."id"
3200 JOIN "privilege"
3201 ON "privilege"."unit_id" = "area"."unit_id"
3202 AND "privilege"."member_id" = "member"."id"
3203 WHERE "issue"."id" = "issue_id_p"
3204 AND "member"."active" AND "privilege"."voting_right";
3205 FOR "member_id_v" IN
3206 SELECT "member_id" FROM "direct_population_snapshot"
3207 WHERE "issue_id" = "issue_id_p"
3208 AND "event" = 'periodic'
3209 LOOP
3210 UPDATE "direct_population_snapshot" SET
3211 "weight" = 1 +
3212 "weight_of_added_delegations_for_population_snapshot"(
3213 "issue_id_p",
3214 "member_id_v",
3215 '{}'
3216 )
3217 WHERE "issue_id" = "issue_id_p"
3218 AND "event" = 'periodic'
3219 AND "member_id" = "member_id_v";
3220 END LOOP;
3221 RETURN;
3222 END;
3223 $$;
3225 COMMENT ON FUNCTION "create_population_snapshot"
3226 ( "issue"."id"%TYPE )
3227 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.';
3230 CREATE FUNCTION "weight_of_added_delegations_for_interest_snapshot"
3231 ( "issue_id_p" "issue"."id"%TYPE,
3232 "member_id_p" "member"."id"%TYPE,
3233 "delegate_member_ids_p" "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
3234 RETURNS "direct_interest_snapshot"."weight"%TYPE
3235 LANGUAGE 'plpgsql' VOLATILE AS $$
3236 DECLARE
3237 "issue_delegation_row" "issue_delegation"%ROWTYPE;
3238 "delegate_member_ids_v" "delegating_interest_snapshot"."delegate_member_ids"%TYPE;
3239 "weight_v" INT4;
3240 "sub_weight_v" INT4;
3241 BEGIN
3242 "weight_v" := 0;
3243 FOR "issue_delegation_row" IN
3244 SELECT * FROM "issue_delegation"
3245 WHERE "trustee_id" = "member_id_p"
3246 AND "issue_id" = "issue_id_p"
3247 LOOP
3248 IF NOT EXISTS (
3249 SELECT NULL FROM "direct_interest_snapshot"
3250 WHERE "issue_id" = "issue_id_p"
3251 AND "event" = 'periodic'
3252 AND "member_id" = "issue_delegation_row"."truster_id"
3253 ) AND NOT EXISTS (
3254 SELECT NULL FROM "delegating_interest_snapshot"
3255 WHERE "issue_id" = "issue_id_p"
3256 AND "event" = 'periodic'
3257 AND "member_id" = "issue_delegation_row"."truster_id"
3258 ) THEN
3259 "delegate_member_ids_v" :=
3260 "member_id_p" || "delegate_member_ids_p";
3261 INSERT INTO "delegating_interest_snapshot" (
3262 "issue_id",
3263 "event",
3264 "member_id",
3265 "scope",
3266 "delegate_member_ids"
3267 ) VALUES (
3268 "issue_id_p",
3269 'periodic',
3270 "issue_delegation_row"."truster_id",
3271 "issue_delegation_row"."scope",
3272 "delegate_member_ids_v"
3273 );
3274 "sub_weight_v" := 1 +
3275 "weight_of_added_delegations_for_interest_snapshot"(
3276 "issue_id_p",
3277 "issue_delegation_row"."truster_id",
3278 "delegate_member_ids_v"
3279 );
3280 UPDATE "delegating_interest_snapshot"
3281 SET "weight" = "sub_weight_v"
3282 WHERE "issue_id" = "issue_id_p"
3283 AND "event" = 'periodic'
3284 AND "member_id" = "issue_delegation_row"."truster_id";
3285 "weight_v" := "weight_v" + "sub_weight_v";
3286 END IF;
3287 END LOOP;
3288 RETURN "weight_v";
3289 END;
3290 $$;
3292 COMMENT ON FUNCTION "weight_of_added_delegations_for_interest_snapshot"
3293 ( "issue"."id"%TYPE,
3294 "member"."id"%TYPE,
3295 "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
3296 IS 'Helper function for "create_interest_snapshot" function';
3299 CREATE FUNCTION "create_interest_snapshot"
3300 ( "issue_id_p" "issue"."id"%TYPE )
3301 RETURNS VOID
3302 LANGUAGE 'plpgsql' VOLATILE AS $$
3303 DECLARE
3304 "member_id_v" "member"."id"%TYPE;
3305 BEGIN
3306 DELETE FROM "direct_interest_snapshot"
3307 WHERE "issue_id" = "issue_id_p"
3308 AND "event" = 'periodic';
3309 DELETE FROM "delegating_interest_snapshot"
3310 WHERE "issue_id" = "issue_id_p"
3311 AND "event" = 'periodic';
3312 DELETE FROM "direct_supporter_snapshot"
3313 WHERE "issue_id" = "issue_id_p"
3314 AND "event" = 'periodic';
3315 INSERT INTO "direct_interest_snapshot"
3316 ("issue_id", "event", "member_id")
3317 SELECT
3318 "issue_id_p" AS "issue_id",
3319 'periodic' AS "event",
3320 "member"."id" AS "member_id"
3321 FROM "issue"
3322 JOIN "area" ON "issue"."area_id" = "area"."id"
3323 JOIN "interest" ON "issue"."id" = "interest"."issue_id"
3324 JOIN "member" ON "interest"."member_id" = "member"."id"
3325 JOIN "privilege"
3326 ON "privilege"."unit_id" = "area"."unit_id"
3327 AND "privilege"."member_id" = "member"."id"
3328 WHERE "issue"."id" = "issue_id_p"
3329 AND "member"."active" AND "privilege"."voting_right";
3330 FOR "member_id_v" IN
3331 SELECT "member_id" FROM "direct_interest_snapshot"
3332 WHERE "issue_id" = "issue_id_p"
3333 AND "event" = 'periodic'
3334 LOOP
3335 UPDATE "direct_interest_snapshot" SET
3336 "weight" = 1 +
3337 "weight_of_added_delegations_for_interest_snapshot"(
3338 "issue_id_p",
3339 "member_id_v",
3340 '{}'
3341 )
3342 WHERE "issue_id" = "issue_id_p"
3343 AND "event" = 'periodic'
3344 AND "member_id" = "member_id_v";
3345 END LOOP;
3346 INSERT INTO "direct_supporter_snapshot"
3347 ( "issue_id", "initiative_id", "event", "member_id",
3348 "draft_id", "informed", "satisfied" )
3349 SELECT
3350 "issue_id_p" AS "issue_id",
3351 "initiative"."id" AS "initiative_id",
3352 'periodic' AS "event",
3353 "supporter"."member_id" AS "member_id",
3354 "supporter"."draft_id" AS "draft_id",
3355 "supporter"."draft_id" = "current_draft"."id" AS "informed",
3356 NOT EXISTS (
3357 SELECT NULL FROM "critical_opinion"
3358 WHERE "initiative_id" = "initiative"."id"
3359 AND "member_id" = "supporter"."member_id"
3360 ) AS "satisfied"
3361 FROM "initiative"
3362 JOIN "supporter"
3363 ON "supporter"."initiative_id" = "initiative"."id"
3364 JOIN "current_draft"
3365 ON "initiative"."id" = "current_draft"."initiative_id"
3366 JOIN "direct_interest_snapshot"
3367 ON "supporter"."member_id" = "direct_interest_snapshot"."member_id"
3368 AND "initiative"."issue_id" = "direct_interest_snapshot"."issue_id"
3369 AND "event" = 'periodic'
3370 WHERE "initiative"."issue_id" = "issue_id_p";
3371 RETURN;
3372 END;
3373 $$;
3375 COMMENT ON FUNCTION "create_interest_snapshot"
3376 ( "issue"."id"%TYPE )
3377 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.';
3380 CREATE FUNCTION "create_snapshot"
3381 ( "issue_id_p" "issue"."id"%TYPE )
3382 RETURNS VOID
3383 LANGUAGE 'plpgsql' VOLATILE AS $$
3384 DECLARE
3385 "initiative_id_v" "initiative"."id"%TYPE;
3386 "suggestion_id_v" "suggestion"."id"%TYPE;
3387 BEGIN
3388 PERFORM "lock_issue"("issue_id_p");
3389 PERFORM "create_population_snapshot"("issue_id_p");
3390 PERFORM "create_interest_snapshot"("issue_id_p");
3391 UPDATE "issue" SET
3392 "snapshot" = now(),
3393 "latest_snapshot_event" = 'periodic',
3394 "population" = (
3395 SELECT coalesce(sum("weight"), 0)
3396 FROM "direct_population_snapshot"
3397 WHERE "issue_id" = "issue_id_p"
3398 AND "event" = 'periodic'
3399 )
3400 WHERE "id" = "issue_id_p";
3401 FOR "initiative_id_v" IN
3402 SELECT "id" FROM "initiative" WHERE "issue_id" = "issue_id_p"
3403 LOOP
3404 UPDATE "initiative" SET
3405 "supporter_count" = (
3406 SELECT coalesce(sum("di"."weight"), 0)
3407 FROM "direct_interest_snapshot" AS "di"
3408 JOIN "direct_supporter_snapshot" AS "ds"
3409 ON "di"."member_id" = "ds"."member_id"
3410 WHERE "di"."issue_id" = "issue_id_p"
3411 AND "di"."event" = 'periodic'
3412 AND "ds"."initiative_id" = "initiative_id_v"
3413 AND "ds"."event" = 'periodic'
3414 ),
3415 "informed_supporter_count" = (
3416 SELECT coalesce(sum("di"."weight"), 0)
3417 FROM "direct_interest_snapshot" AS "di"
3418 JOIN "direct_supporter_snapshot" AS "ds"
3419 ON "di"."member_id" = "ds"."member_id"
3420 WHERE "di"."issue_id" = "issue_id_p"
3421 AND "di"."event" = 'periodic'
3422 AND "ds"."initiative_id" = "initiative_id_v"
3423 AND "ds"."event" = 'periodic'
3424 AND "ds"."informed"
3425 ),
3426 "satisfied_supporter_count" = (
3427 SELECT coalesce(sum("di"."weight"), 0)
3428 FROM "direct_interest_snapshot" AS "di"
3429 JOIN "direct_supporter_snapshot" AS "ds"
3430 ON "di"."member_id" = "ds"."member_id"
3431 WHERE "di"."issue_id" = "issue_id_p"
3432 AND "di"."event" = 'periodic'
3433 AND "ds"."initiative_id" = "initiative_id_v"
3434 AND "ds"."event" = 'periodic'
3435 AND "ds"."satisfied"
3436 ),
3437 "satisfied_informed_supporter_count" = (
3438 SELECT coalesce(sum("di"."weight"), 0)
3439 FROM "direct_interest_snapshot" AS "di"
3440 JOIN "direct_supporter_snapshot" AS "ds"
3441 ON "di"."member_id" = "ds"."member_id"
3442 WHERE "di"."issue_id" = "issue_id_p"
3443 AND "di"."event" = 'periodic'
3444 AND "ds"."initiative_id" = "initiative_id_v"
3445 AND "ds"."event" = 'periodic'
3446 AND "ds"."informed"
3447 AND "ds"."satisfied"
3448 )
3449 WHERE "id" = "initiative_id_v";
3450 FOR "suggestion_id_v" IN
3451 SELECT "id" FROM "suggestion"
3452 WHERE "initiative_id" = "initiative_id_v"
3453 LOOP
3454 UPDATE "suggestion" SET
3455 "minus2_unfulfilled_count" = (
3456 SELECT coalesce(sum("snapshot"."weight"), 0)
3457 FROM "issue" CROSS JOIN "opinion"
3458 JOIN "direct_interest_snapshot" AS "snapshot"
3459 ON "snapshot"."issue_id" = "issue"."id"
3460 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3461 AND "snapshot"."member_id" = "opinion"."member_id"
3462 WHERE "issue"."id" = "issue_id_p"
3463 AND "opinion"."suggestion_id" = "suggestion_id_v"
3464 AND "opinion"."degree" = -2
3465 AND "opinion"."fulfilled" = FALSE
3466 ),
3467 "minus2_fulfilled_count" = (
3468 SELECT coalesce(sum("snapshot"."weight"), 0)
3469 FROM "issue" CROSS JOIN "opinion"
3470 JOIN "direct_interest_snapshot" AS "snapshot"
3471 ON "snapshot"."issue_id" = "issue"."id"
3472 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3473 AND "snapshot"."member_id" = "opinion"."member_id"
3474 WHERE "issue"."id" = "issue_id_p"
3475 AND "opinion"."suggestion_id" = "suggestion_id_v"
3476 AND "opinion"."degree" = -2
3477 AND "opinion"."fulfilled" = TRUE
3478 ),
3479 "minus1_unfulfilled_count" = (
3480 SELECT coalesce(sum("snapshot"."weight"), 0)
3481 FROM "issue" CROSS JOIN "opinion"
3482 JOIN "direct_interest_snapshot" AS "snapshot"
3483 ON "snapshot"."issue_id" = "issue"."id"
3484 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3485 AND "snapshot"."member_id" = "opinion"."member_id"
3486 WHERE "issue"."id" = "issue_id_p"
3487 AND "opinion"."suggestion_id" = "suggestion_id_v"
3488 AND "opinion"."degree" = -1
3489 AND "opinion"."fulfilled" = FALSE
3490 ),
3491 "minus1_fulfilled_count" = (
3492 SELECT coalesce(sum("snapshot"."weight"), 0)
3493 FROM "issue" CROSS JOIN "opinion"
3494 JOIN "direct_interest_snapshot" AS "snapshot"
3495 ON "snapshot"."issue_id" = "issue"."id"
3496 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3497 AND "snapshot"."member_id" = "opinion"."member_id"
3498 WHERE "issue"."id" = "issue_id_p"
3499 AND "opinion"."suggestion_id" = "suggestion_id_v"
3500 AND "opinion"."degree" = -1
3501 AND "opinion"."fulfilled" = TRUE
3502 ),
3503 "plus1_unfulfilled_count" = (
3504 SELECT coalesce(sum("snapshot"."weight"), 0)
3505 FROM "issue" CROSS JOIN "opinion"
3506 JOIN "direct_interest_snapshot" AS "snapshot"
3507 ON "snapshot"."issue_id" = "issue"."id"
3508 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3509 AND "snapshot"."member_id" = "opinion"."member_id"
3510 WHERE "issue"."id" = "issue_id_p"
3511 AND "opinion"."suggestion_id" = "suggestion_id_v"
3512 AND "opinion"."degree" = 1
3513 AND "opinion"."fulfilled" = FALSE
3514 ),
3515 "plus1_fulfilled_count" = (
3516 SELECT coalesce(sum("snapshot"."weight"), 0)
3517 FROM "issue" CROSS JOIN "opinion"
3518 JOIN "direct_interest_snapshot" AS "snapshot"
3519 ON "snapshot"."issue_id" = "issue"."id"
3520 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3521 AND "snapshot"."member_id" = "opinion"."member_id"
3522 WHERE "issue"."id" = "issue_id_p"
3523 AND "opinion"."suggestion_id" = "suggestion_id_v"
3524 AND "opinion"."degree" = 1
3525 AND "opinion"."fulfilled" = TRUE
3526 ),
3527 "plus2_unfulfilled_count" = (
3528 SELECT coalesce(sum("snapshot"."weight"), 0)
3529 FROM "issue" CROSS JOIN "opinion"
3530 JOIN "direct_interest_snapshot" AS "snapshot"
3531 ON "snapshot"."issue_id" = "issue"."id"
3532 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3533 AND "snapshot"."member_id" = "opinion"."member_id"
3534 WHERE "issue"."id" = "issue_id_p"
3535 AND "opinion"."suggestion_id" = "suggestion_id_v"
3536 AND "opinion"."degree" = 2
3537 AND "opinion"."fulfilled" = FALSE
3538 ),
3539 "plus2_fulfilled_count" = (
3540 SELECT coalesce(sum("snapshot"."weight"), 0)
3541 FROM "issue" CROSS JOIN "opinion"
3542 JOIN "direct_interest_snapshot" AS "snapshot"
3543 ON "snapshot"."issue_id" = "issue"."id"
3544 AND "snapshot"."event" = "issue"."latest_snapshot_event"
3545 AND "snapshot"."member_id" = "opinion"."member_id"
3546 WHERE "issue"."id" = "issue_id_p"
3547 AND "opinion"."suggestion_id" = "suggestion_id_v"
3548 AND "opinion"."degree" = 2
3549 AND "opinion"."fulfilled" = TRUE
3550 )
3551 WHERE "suggestion"."id" = "suggestion_id_v";
3552 END LOOP;
3553 END LOOP;
3554 RETURN;
3555 END;
3556 $$;
3558 COMMENT ON FUNCTION "create_snapshot"
3559 ( "issue"."id"%TYPE )
3560 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.';
3563 CREATE FUNCTION "set_snapshot_event"
3564 ( "issue_id_p" "issue"."id"%TYPE,
3565 "event_p" "snapshot_event" )
3566 RETURNS VOID
3567 LANGUAGE 'plpgsql' VOLATILE AS $$
3568 DECLARE
3569 "event_v" "issue"."latest_snapshot_event"%TYPE;
3570 BEGIN
3571 SELECT "latest_snapshot_event" INTO "event_v" FROM "issue"
3572 WHERE "id" = "issue_id_p" FOR UPDATE;
3573 UPDATE "issue" SET "latest_snapshot_event" = "event_p"
3574 WHERE "id" = "issue_id_p";
3575 UPDATE "direct_population_snapshot" SET "event" = "event_p"
3576 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3577 UPDATE "delegating_population_snapshot" SET "event" = "event_p"
3578 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3579 UPDATE "direct_interest_snapshot" SET "event" = "event_p"
3580 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3581 UPDATE "delegating_interest_snapshot" SET "event" = "event_p"
3582 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3583 UPDATE "direct_supporter_snapshot" SET "event" = "event_p"
3584 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
3585 RETURN;
3586 END;
3587 $$;
3589 COMMENT ON FUNCTION "set_snapshot_event"
3590 ( "issue"."id"%TYPE,
3591 "snapshot_event" )
3592 IS 'Change "event" attribute of the previous ''periodic'' snapshot';
3596 ---------------------
3597 -- Freezing issues --
3598 ---------------------
3600 CREATE FUNCTION "freeze_after_snapshot"
3601 ( "issue_id_p" "issue"."id"%TYPE )
3602 RETURNS VOID
3603 LANGUAGE 'plpgsql' VOLATILE AS $$
3604 DECLARE
3605 "issue_row" "issue"%ROWTYPE;
3606 "policy_row" "policy"%ROWTYPE;
3607 "initiative_row" "initiative"%ROWTYPE;
3608 BEGIN
3609 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3610 SELECT * INTO "policy_row"
3611 FROM "policy" WHERE "id" = "issue_row"."policy_id";
3612 PERFORM "set_snapshot_event"("issue_id_p", 'full_freeze');
3613 FOR "initiative_row" IN
3614 SELECT * FROM "initiative"
3615 WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
3616 LOOP
3617 IF
3618 "initiative_row"."polling" OR (
3619 "initiative_row"."satisfied_supporter_count" > 0 AND
3620 "initiative_row"."satisfied_supporter_count" *
3621 "policy_row"."initiative_quorum_den" >=
3622 "issue_row"."population" * "policy_row"."initiative_quorum_num"
3623 )
3624 THEN
3625 UPDATE "initiative" SET "admitted" = TRUE
3626 WHERE "id" = "initiative_row"."id";
3627 ELSE
3628 UPDATE "initiative" SET "admitted" = FALSE
3629 WHERE "id" = "initiative_row"."id";
3630 END IF;
3631 END LOOP;
3632 IF EXISTS (
3633 SELECT NULL FROM "initiative"
3634 WHERE "issue_id" = "issue_id_p" AND "admitted" = TRUE
3635 ) THEN
3636 UPDATE "issue" SET
3637 "state" = 'voting',
3638 "accepted" = coalesce("accepted", now()),
3639 "half_frozen" = coalesce("half_frozen", now()),
3640 "fully_frozen" = now()
3641 WHERE "id" = "issue_id_p";
3642 ELSE
3643 UPDATE "issue" SET
3644 "state" = 'canceled_no_initiative_admitted',
3645 "accepted" = coalesce("accepted", now()),
3646 "half_frozen" = coalesce("half_frozen", now()),
3647 "fully_frozen" = now(),
3648 "closed" = now(),
3649 "ranks_available" = TRUE
3650 WHERE "id" = "issue_id_p";
3651 -- NOTE: The following DELETE statements have effect only when
3652 -- issue state has been manipulated
3653 DELETE FROM "direct_voter" WHERE "issue_id" = "issue_id_p";
3654 DELETE FROM "delegating_voter" WHERE "issue_id" = "issue_id_p";
3655 DELETE FROM "battle" WHERE "issue_id" = "issue_id_p";
3656 END IF;
3657 RETURN;
3658 END;
3659 $$;
3661 COMMENT ON FUNCTION "freeze_after_snapshot"
3662 ( "issue"."id"%TYPE )
3663 IS 'This function freezes an issue (fully) and starts voting, but must only be called when "create_snapshot" was called in the same transaction.';
3666 CREATE FUNCTION "manual_freeze"("issue_id_p" "issue"."id"%TYPE)
3667 RETURNS VOID
3668 LANGUAGE 'plpgsql' VOLATILE AS $$
3669 DECLARE
3670 "issue_row" "issue"%ROWTYPE;
3671 BEGIN
3672 PERFORM "create_snapshot"("issue_id_p");
3673 PERFORM "freeze_after_snapshot"("issue_id_p");
3674 RETURN;
3675 END;
3676 $$;
3678 COMMENT ON FUNCTION "manual_freeze"
3679 ( "issue"."id"%TYPE )
3680 IS 'Freeze an issue manually (fully) and start voting';
3684 -----------------------
3685 -- Counting of votes --
3686 -----------------------
3689 CREATE FUNCTION "weight_of_added_vote_delegations"
3690 ( "issue_id_p" "issue"."id"%TYPE,
3691 "member_id_p" "member"."id"%TYPE,
3692 "delegate_member_ids_p" "delegating_voter"."delegate_member_ids"%TYPE )
3693 RETURNS "direct_voter"."weight"%TYPE
3694 LANGUAGE 'plpgsql' VOLATILE AS $$
3695 DECLARE
3696 "issue_delegation_row" "issue_delegation"%ROWTYPE;
3697 "delegate_member_ids_v" "delegating_voter"."delegate_member_ids"%TYPE;
3698 "weight_v" INT4;
3699 "sub_weight_v" INT4;
3700 BEGIN
3701 "weight_v" := 0;
3702 FOR "issue_delegation_row" IN
3703 SELECT * FROM "issue_delegation"
3704 WHERE "trustee_id" = "member_id_p"
3705 AND "issue_id" = "issue_id_p"
3706 LOOP
3707 IF NOT EXISTS (
3708 SELECT NULL FROM "direct_voter"
3709 WHERE "member_id" = "issue_delegation_row"."truster_id"
3710 AND "issue_id" = "issue_id_p"
3711 ) AND NOT EXISTS (
3712 SELECT NULL FROM "delegating_voter"
3713 WHERE "member_id" = "issue_delegation_row"."truster_id"
3714 AND "issue_id" = "issue_id_p"
3715 ) THEN
3716 "delegate_member_ids_v" :=
3717 "member_id_p" || "delegate_member_ids_p";
3718 INSERT INTO "delegating_voter" (
3719 "issue_id",
3720 "member_id",
3721 "scope",
3722 "delegate_member_ids"
3723 ) VALUES (
3724 "issue_id_p",
3725 "issue_delegation_row"."truster_id",
3726 "issue_delegation_row"."scope",
3727 "delegate_member_ids_v"
3728 );
3729 "sub_weight_v" := 1 +
3730 "weight_of_added_vote_delegations"(
3731 "issue_id_p",
3732 "issue_delegation_row"."truster_id",
3733 "delegate_member_ids_v"
3734 );
3735 UPDATE "delegating_voter"
3736 SET "weight" = "sub_weight_v"
3737 WHERE "issue_id" = "issue_id_p"
3738 AND "member_id" = "issue_delegation_row"."truster_id";
3739 "weight_v" := "weight_v" + "sub_weight_v";
3740 END IF;
3741 END LOOP;
3742 RETURN "weight_v";
3743 END;
3744 $$;
3746 COMMENT ON FUNCTION "weight_of_added_vote_delegations"
3747 ( "issue"."id"%TYPE,
3748 "member"."id"%TYPE,
3749 "delegating_voter"."delegate_member_ids"%TYPE )
3750 IS 'Helper function for "add_vote_delegations" function';
3753 CREATE FUNCTION "add_vote_delegations"
3754 ( "issue_id_p" "issue"."id"%TYPE )
3755 RETURNS VOID
3756 LANGUAGE 'plpgsql' VOLATILE AS $$
3757 DECLARE
3758 "member_id_v" "member"."id"%TYPE;
3759 BEGIN
3760 FOR "member_id_v" IN
3761 SELECT "member_id" FROM "direct_voter"
3762 WHERE "issue_id" = "issue_id_p"
3763 LOOP
3764 UPDATE "direct_voter" SET
3765 "weight" = "weight" + "weight_of_added_vote_delegations"(
3766 "issue_id_p",
3767 "member_id_v",
3768 '{}'
3769 )
3770 WHERE "member_id" = "member_id_v"
3771 AND "issue_id" = "issue_id_p";
3772 END LOOP;
3773 RETURN;
3774 END;
3775 $$;
3777 COMMENT ON FUNCTION "add_vote_delegations"
3778 ( "issue_id_p" "issue"."id"%TYPE )
3779 IS 'Helper function for "close_voting" function';
3782 CREATE FUNCTION "close_voting"("issue_id_p" "issue"."id"%TYPE)
3783 RETURNS VOID
3784 LANGUAGE 'plpgsql' VOLATILE AS $$
3785 DECLARE
3786 "area_id_v" "area"."id"%TYPE;
3787 "unit_id_v" "unit"."id"%TYPE;
3788 "member_id_v" "member"."id"%TYPE;
3789 BEGIN
3790 PERFORM "lock_issue"("issue_id_p");
3791 SELECT "area_id" INTO "area_id_v" FROM "issue" WHERE "id" = "issue_id_p";
3792 SELECT "unit_id" INTO "unit_id_v" FROM "area" WHERE "id" = "area_id_v";
3793 -- delete delegating votes (in cases of manual reset of issue state):
3794 DELETE FROM "delegating_voter"
3795 WHERE "issue_id" = "issue_id_p";
3796 -- delete votes from non-privileged voters:
3797 DELETE FROM "direct_voter"
3798 USING (
3799 SELECT
3800 "direct_voter"."member_id"
3801 FROM "direct_voter"
3802 JOIN "member" ON "direct_voter"."member_id" = "member"."id"
3803 LEFT JOIN "privilege"
3804 ON "privilege"."unit_id" = "unit_id_v"
3805 AND "privilege"."member_id" = "direct_voter"."member_id"
3806 WHERE "direct_voter"."issue_id" = "issue_id_p" AND (
3807 "member"."active" = FALSE OR
3808 "privilege"."voting_right" ISNULL OR
3809 "privilege"."voting_right" = FALSE
3810 )
3811 ) AS "subquery"
3812 WHERE "direct_voter"."issue_id" = "issue_id_p"
3813 AND "direct_voter"."member_id" = "subquery"."member_id";
3814 -- consider delegations:
3815 UPDATE "direct_voter" SET "weight" = 1
3816 WHERE "issue_id" = "issue_id_p";
3817 PERFORM "add_vote_delegations"("issue_id_p");
3818 -- set voter count and mark issue as being calculated:
3819 UPDATE "issue" SET
3820 "state" = 'calculation',
3821 "closed" = now(),
3822 "voter_count" = (
3823 SELECT coalesce(sum("weight"), 0)
3824 FROM "direct_voter" WHERE "issue_id" = "issue_id_p"
3825 )
3826 WHERE "id" = "issue_id_p";
3827 -- materialize battle_view:
3828 -- NOTE: "closed" column of issue must be set at this point
3829 DELETE FROM "battle" WHERE "issue_id" = "issue_id_p";
3830 INSERT INTO "battle" (
3831 "issue_id",
3832 "winning_initiative_id", "losing_initiative_id",
3833 "count"
3834 ) SELECT
3835 "issue_id",
3836 "winning_initiative_id", "losing_initiative_id",
3837 "count"
3838 FROM "battle_view" WHERE "issue_id" = "issue_id_p";
3839 -- copy "positive_votes" and "negative_votes" from "battle" table:
3840 UPDATE "initiative" SET
3841 "positive_votes" = "battle_win"."count",
3842 "negative_votes" = "battle_lose"."count"
3843 FROM "battle" AS "battle_win", "battle" AS "battle_lose"
3844 WHERE
3845 "battle_win"."issue_id" = "issue_id_p" AND
3846 "battle_win"."winning_initiative_id" = "initiative"."id" AND
3847 "battle_win"."losing_initiative_id" ISNULL AND
3848 "battle_lose"."issue_id" = "issue_id_p" AND
3849 "battle_lose"."losing_initiative_id" = "initiative"."id" AND
3850 "battle_lose"."winning_initiative_id" ISNULL;
3851 END;
3852 $$;
3854 COMMENT ON FUNCTION "close_voting"
3855 ( "issue"."id"%TYPE )
3856 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.';
3859 CREATE FUNCTION "defeat_strength"
3860 ( "positive_votes_p" INT4, "negative_votes_p" INT4 )
3861 RETURNS INT8
3862 LANGUAGE 'plpgsql' IMMUTABLE AS $$
3863 BEGIN
3864 IF "positive_votes_p" > "negative_votes_p" THEN
3865 RETURN ("positive_votes_p"::INT8 << 31) - "negative_votes_p"::INT8;
3866 ELSIF "positive_votes_p" = "negative_votes_p" THEN
3867 RETURN 0;
3868 ELSE
3869 RETURN -1;
3870 END IF;
3871 END;
3872 $$;
3874 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';
3877 CREATE FUNCTION "calculate_ranks"("issue_id_p" "issue"."id"%TYPE)
3878 RETURNS VOID
3879 LANGUAGE 'plpgsql' VOLATILE AS $$
3880 DECLARE
3881 "issue_row" "issue"%ROWTYPE;
3882 "policy_row" "policy"%ROWTYPE;
3883 "dimension_v" INTEGER;
3884 "vote_matrix" INT4[][]; -- absolute votes
3885 "matrix" INT8[][]; -- defeat strength / best paths
3886 "i" INTEGER;
3887 "j" INTEGER;
3888 "k" INTEGER;
3889 "battle_row" "battle"%ROWTYPE;
3890 "rank_ary" INT4[];
3891 "rank_v" INT4;
3892 "done_v" INTEGER;
3893 "winners_ary" INTEGER[];
3894 "initiative_id_v" "initiative"."id"%TYPE;
3895 BEGIN
3896 SELECT * INTO "issue_row"
3897 FROM "issue" WHERE "id" = "issue_id_p"
3898 FOR UPDATE;
3899 SELECT * INTO "policy_row"
3900 FROM "policy" WHERE "id" = "issue_row"."policy_id";
3901 SELECT count(1) INTO "dimension_v"
3902 FROM "battle_participant" WHERE "issue_id" = "issue_id_p";
3903 -- Create "vote_matrix" with absolute number of votes in pairwise
3904 -- comparison:
3905 "vote_matrix" := array_fill(NULL::INT4, ARRAY["dimension_v", "dimension_v"]);
3906 "i" := 1;
3907 "j" := 2;
3908 FOR "battle_row" IN
3909 SELECT * FROM "battle" WHERE "issue_id" = "issue_id_p"
3910 ORDER BY
3911 "winning_initiative_id" NULLS LAST,
3912 "losing_initiative_id" NULLS LAST
3913 LOOP
3914 "vote_matrix"["i"]["j"] := "battle_row"."count";
3915 IF "j" = "dimension_v" THEN
3916 "i" := "i" + 1;
3917 "j" := 1;
3918 ELSE
3919 "j" := "j" + 1;
3920 IF "j" = "i" THEN
3921 "j" := "j" + 1;
3922 END IF;
3923 END IF;
3924 END LOOP;
3925 IF "i" != "dimension_v" OR "j" != "dimension_v" + 1 THEN
3926 RAISE EXCEPTION 'Wrong battle count (should not happen)';
3927 END IF;
3928 -- Store defeat strengths in "matrix" using "defeat_strength"
3929 -- function:
3930 "matrix" := array_fill(NULL::INT8, ARRAY["dimension_v", "dimension_v"]);
3931 "i" := 1;
3932 LOOP
3933 "j" := 1;
3934 LOOP
3935 IF "i" != "j" THEN
3936 "matrix"["i"]["j"] := "defeat_strength"(
3937 "vote_matrix"["i"]["j"],
3938 "vote_matrix"["j"]["i"]
3939 );
3940 END IF;
3941 EXIT WHEN "j" = "dimension_v";
3942 "j" := "j" + 1;
3943 END LOOP;
3944 EXIT WHEN "i" = "dimension_v";
3945 "i" := "i" + 1;
3946 END LOOP;
3947 -- Find best paths:
3948 "i" := 1;
3949 LOOP
3950 "j" := 1;
3951 LOOP
3952 IF "i" != "j" THEN
3953 "k" := 1;
3954 LOOP
3955 IF "i" != "k" AND "j" != "k" THEN
3956 IF "matrix"["j"]["i"] < "matrix"["i"]["k"] THEN
3957 IF "matrix"["j"]["i"] > "matrix"["j"]["k"] THEN
3958 "matrix"["j"]["k"] := "matrix"["j"]["i"];
3959 END IF;
3960 ELSE
3961 IF "matrix"["i"]["k"] > "matrix"["j"]["k"] THEN
3962 "matrix"["j"]["k"] := "matrix"["i"]["k"];
3963 END IF;
3964 END IF;
3965 END IF;
3966 EXIT WHEN "k" = "dimension_v";
3967 "k" := "k" + 1;
3968 END LOOP;
3969 END IF;
3970 EXIT WHEN "j" = "dimension_v";
3971 "j" := "j" + 1;
3972 END LOOP;
3973 EXIT WHEN "i" = "dimension_v";
3974 "i" := "i" + 1;
3975 END LOOP;
3976 -- Determine order of winners:
3977 "rank_ary" := array_fill(NULL::INT4, ARRAY["dimension_v"]);
3978 "rank_v" := 1;
3979 "done_v" := 0;
3980 LOOP
3981 "winners_ary" := '{}';
3982 "i" := 1;
3983 LOOP
3984 IF "rank_ary"["i"] ISNULL THEN
3985 "j" := 1;
3986 LOOP
3987 IF
3988 "i" != "j" AND
3989 "rank_ary"["j"] ISNULL AND
3990 "matrix"["j"]["i"] > "matrix"["i"]["j"]
3991 THEN
3992 -- someone else is better
3993 EXIT;
3994 END IF;
3995 IF "j" = "dimension_v" THEN
3996 -- noone is better
3997 "winners_ary" := "winners_ary" || "i";
3998 EXIT;
3999 END IF;
4000 "j" := "j" + 1;
4001 END LOOP;
4002 END IF;
4003 EXIT WHEN "i" = "dimension_v";
4004 "i" := "i" + 1;
4005 END LOOP;
4006 "i" := 1;
4007 LOOP
4008 "rank_ary"["winners_ary"["i"]] := "rank_v";
4009 "done_v" := "done_v" + 1;
4010 EXIT WHEN "i" = array_upper("winners_ary", 1);
4011 "i" := "i" + 1;
4012 END LOOP;
4013 EXIT WHEN "done_v" = "dimension_v";
4014 "rank_v" := "rank_v" + 1;
4015 END LOOP;
4016 -- write preliminary results:
4017 "i" := 1;
4018 FOR "initiative_id_v" IN
4019 SELECT "id" FROM "initiative"
4020 WHERE "issue_id" = "issue_id_p" AND "admitted"
4021 ORDER BY "id"
4022 LOOP
4023 UPDATE "initiative" SET
4024 "direct_majority" =
4025 CASE WHEN "policy_row"."direct_majority_strict" THEN
4026 "positive_votes" * "policy_row"."direct_majority_den" >
4027 "policy_row"."direct_majority_num" * ("positive_votes"+"negative_votes")
4028 ELSE
4029 "positive_votes" * "policy_row"."direct_majority_den" >=
4030 "policy_row"."direct_majority_num" * ("positive_votes"+"negative_votes")
4031 END
4032 AND "positive_votes" >= "policy_row"."direct_majority_positive"
4033 AND "issue_row"."voter_count"-"negative_votes" >=
4034 "policy_row"."direct_majority_non_negative",
4035 "indirect_majority" =
4036 CASE WHEN "policy_row"."indirect_majority_strict" THEN
4037 "positive_votes" * "policy_row"."indirect_majority_den" >
4038 "policy_row"."indirect_majority_num" * ("positive_votes"+"negative_votes")
4039 ELSE
4040 "positive_votes" * "policy_row"."indirect_majority_den" >=
4041 "policy_row"."indirect_majority_num" * ("positive_votes"+"negative_votes")
4042 END
4043 AND "positive_votes" >= "policy_row"."indirect_majority_positive"
4044 AND "issue_row"."voter_count"-"negative_votes" >=
4045 "policy_row"."indirect_majority_non_negative",
4046 "schulze_rank" = "rank_ary"["i"],
4047 "better_than_status_quo" = "rank_ary"["i"] < "rank_ary"["dimension_v"],
4048 "worse_than_status_quo" = "rank_ary"["i"] > "rank_ary"["dimension_v"],
4049 "multistage_majority" = "rank_ary"["i"] >= "rank_ary"["dimension_v"],
4050 "reverse_beat_path" = "matrix"["dimension_v"]["i"] >= 0,
4051 "eligible" = FALSE,
4052 "winner" = FALSE,
4053 "rank" = NULL -- NOTE: in cases of manual reset of issue state
4054 WHERE "id" = "initiative_id_v";
4055 "i" := "i" + 1;
4056 END LOOP;
4057 IF "i" != "dimension_v" THEN
4058 RAISE EXCEPTION 'Wrong winner count (should not happen)';
4059 END IF;
4060 -- take indirect majorities into account:
4061 LOOP
4062 UPDATE "initiative" SET "indirect_majority" = TRUE
4063 FROM (
4064 SELECT "new_initiative"."id" AS "initiative_id"
4065 FROM "initiative" "old_initiative"
4066 JOIN "initiative" "new_initiative"
4067 ON "new_initiative"."issue_id" = "issue_id_p"
4068 AND "new_initiative"."indirect_majority" = FALSE
4069 JOIN "battle" "battle_win"
4070 ON "battle_win"."issue_id" = "issue_id_p"
4071 AND "battle_win"."winning_initiative_id" = "new_initiative"."id"
4072 AND "battle_win"."losing_initiative_id" = "old_initiative"."id"
4073 JOIN "battle" "battle_lose"
4074 ON "battle_lose"."issue_id" = "issue_id_p"
4075 AND "battle_lose"."losing_initiative_id" = "new_initiative"."id"
4076 AND "battle_lose"."winning_initiative_id" = "old_initiative"."id"
4077 WHERE "old_initiative"."issue_id" = "issue_id_p"
4078 AND "old_initiative"."indirect_majority" = TRUE
4079 AND CASE WHEN "policy_row"."indirect_majority_strict" THEN
4080 "battle_win"."count" * "policy_row"."indirect_majority_den" >
4081 "policy_row"."indirect_majority_num" *
4082 ("battle_win"."count"+"battle_lose"."count")
4083 ELSE
4084 "battle_win"."count" * "policy_row"."indirect_majority_den" >=
4085 "policy_row"."indirect_majority_num" *
4086 ("battle_win"."count"+"battle_lose"."count")
4087 END
4088 AND "battle_win"."count" >= "policy_row"."indirect_majority_positive"
4089 AND "issue_row"."voter_count"-"battle_lose"."count" >=
4090 "policy_row"."indirect_majority_non_negative"
4091 ) AS "subquery"
4092 WHERE "id" = "subquery"."initiative_id";
4093 EXIT WHEN NOT FOUND;
4094 END LOOP;
4095 -- set "multistage_majority" for remaining matching initiatives:
4096 UPDATE "initiative" SET "multistage_majority" = TRUE
4097 FROM (
4098 SELECT "losing_initiative"."id" AS "initiative_id"
4099 FROM "initiative" "losing_initiative"
4100 JOIN "initiative" "winning_initiative"
4101 ON "winning_initiative"."issue_id" = "issue_id_p"
4102 AND "winning_initiative"."admitted"
4103 JOIN "battle" "battle_win"
4104 ON "battle_win"."issue_id" = "issue_id_p"
4105 AND "battle_win"."winning_initiative_id" = "winning_initiative"."id"
4106 AND "battle_win"."losing_initiative_id" = "losing_initiative"."id"
4107 JOIN "battle" "battle_lose"
4108 ON "battle_lose"."issue_id" = "issue_id_p"
4109 AND "battle_lose"."losing_initiative_id" = "winning_initiative"."id"
4110 AND "battle_lose"."winning_initiative_id" = "losing_initiative"."id"
4111 WHERE "losing_initiative"."issue_id" = "issue_id_p"
4112 AND "losing_initiative"."admitted"
4113 AND "winning_initiative"."schulze_rank" <
4114 "losing_initiative"."schulze_rank"
4115 AND "battle_win"."count" > "battle_lose"."count"
4116 AND (
4117 "battle_win"."count" > "winning_initiative"."positive_votes" OR
4118 "battle_lose"."count" < "losing_initiative"."negative_votes" )
4119 ) AS "subquery"
4120 WHERE "id" = "subquery"."initiative_id";
4121 -- mark eligible initiatives:
4122 UPDATE "initiative" SET "eligible" = TRUE
4123 WHERE "issue_id" = "issue_id_p"
4124 AND "initiative"."direct_majority"
4125 AND "initiative"."indirect_majority"
4126 AND "initiative"."better_than_status_quo"
4127 AND (
4128 "policy_row"."no_multistage_majority" = FALSE OR
4129 "initiative"."multistage_majority" = FALSE )
4130 AND (
4131 "policy_row"."no_reverse_beat_path" = FALSE OR
4132 "initiative"."reverse_beat_path" = FALSE );
4133 -- mark final winner:
4134 UPDATE "initiative" SET "winner" = TRUE
4135 FROM (
4136 SELECT "id" AS "initiative_id"
4137 FROM "initiative"
4138 WHERE "issue_id" = "issue_id_p" AND "eligible"
4139 ORDER BY
4140 "schulze_rank",
4141 "vote_ratio"("positive_votes", "negative_votes"),
4142 "id"
4143 LIMIT 1
4144 ) AS "subquery"
4145 WHERE "id" = "subquery"."initiative_id";
4146 -- write (final) ranks:
4147 "rank_v" := 1;
4148 FOR "initiative_id_v" IN
4149 SELECT "id"
4150 FROM "initiative"
4151 WHERE "issue_id" = "issue_id_p" AND "admitted"
4152 ORDER BY
4153 "winner" DESC,
4154 "eligible" DESC,
4155 "schulze_rank",
4156 "vote_ratio"("positive_votes", "negative_votes"),
4157 "id"
4158 LOOP
4159 UPDATE "initiative" SET "rank" = "rank_v"
4160 WHERE "id" = "initiative_id_v";
4161 "rank_v" := "rank_v" + 1;
4162 END LOOP;
4163 -- set schulze rank of status quo and mark issue as finished:
4164 UPDATE "issue" SET
4165 "status_quo_schulze_rank" = "rank_ary"["dimension_v"],
4166 "state" =
4167 CASE WHEN EXISTS (
4168 SELECT NULL FROM "initiative"
4169 WHERE "issue_id" = "issue_id_p" AND "winner"
4170 ) THEN
4171 'finished_with_winner'::"issue_state"
4172 ELSE
4173 'finished_without_winner'::"issue_state"
4174 END,
4175 "ranks_available" = TRUE
4176 WHERE "id" = "issue_id_p";
4177 RETURN;
4178 END;
4179 $$;
4181 COMMENT ON FUNCTION "calculate_ranks"
4182 ( "issue"."id"%TYPE )
4183 IS 'Determine ranking (Votes have to be counted first)';
4187 -----------------------------
4188 -- Automatic state changes --
4189 -----------------------------
4192 CREATE FUNCTION "check_issue"
4193 ( "issue_id_p" "issue"."id"%TYPE )
4194 RETURNS VOID
4195 LANGUAGE 'plpgsql' VOLATILE AS $$
4196 DECLARE
4197 "issue_row" "issue"%ROWTYPE;
4198 "policy_row" "policy"%ROWTYPE;
4199 BEGIN
4200 PERFORM "lock_issue"("issue_id_p");
4201 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
4202 -- only process open issues:
4203 IF "issue_row"."closed" ISNULL THEN
4204 SELECT * INTO "policy_row" FROM "policy"
4205 WHERE "id" = "issue_row"."policy_id";
4206 -- create a snapshot, unless issue is already fully frozen:
4207 IF "issue_row"."fully_frozen" ISNULL THEN
4208 PERFORM "create_snapshot"("issue_id_p");
4209 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
4210 END IF;
4211 -- eventually close or accept issues, which have not been accepted:
4212 IF "issue_row"."accepted" ISNULL THEN
4213 IF EXISTS (
4214 SELECT NULL FROM "initiative"
4215 WHERE "issue_id" = "issue_id_p"
4216 AND "supporter_count" > 0
4217 AND "supporter_count" * "policy_row"."issue_quorum_den"
4218 >= "issue_row"."population" * "policy_row"."issue_quorum_num"
4219 ) THEN
4220 -- accept issues, if supporter count is high enough
4221 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
4222 -- NOTE: "issue_row" used later
4223 "issue_row"."state" := 'discussion';
4224 "issue_row"."accepted" := now();
4225 UPDATE "issue" SET
4226 "state" = "issue_row"."state",
4227 "accepted" = "issue_row"."accepted"
4228 WHERE "id" = "issue_row"."id";
4229 ELSIF
4230 now() >= "issue_row"."created" + "issue_row"."admission_time"
4231 THEN
4232 -- close issues, if admission time has expired
4233 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
4234 UPDATE "issue" SET
4235 "state" = 'canceled_issue_not_accepted',
4236 "closed" = now()
4237 WHERE "id" = "issue_row"."id";
4238 END IF;
4239 END IF;
4240 -- eventually half freeze issues:
4241 IF
4242 -- NOTE: issue can't be closed at this point, if it has been accepted
4243 "issue_row"."accepted" NOTNULL AND
4244 "issue_row"."half_frozen" ISNULL
4245 THEN
4246 IF
4247 now() >= "issue_row"."accepted" + "issue_row"."discussion_time"
4248 THEN
4249 PERFORM "set_snapshot_event"("issue_id_p", 'half_freeze');
4250 -- NOTE: "issue_row" used later
4251 "issue_row"."state" := 'verification';
4252 "issue_row"."half_frozen" := now();
4253 UPDATE "issue" SET
4254 "state" = "issue_row"."state",
4255 "half_frozen" = "issue_row"."half_frozen"
4256 WHERE "id" = "issue_row"."id";
4257 END IF;
4258 END IF;
4259 -- close issues after some time, if all initiatives have been revoked:
4260 IF
4261 "issue_row"."closed" ISNULL AND
4262 NOT EXISTS (
4263 -- all initiatives are revoked
4264 SELECT NULL FROM "initiative"
4265 WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
4266 ) AND (
4267 -- and issue has not been accepted yet
4268 "issue_row"."accepted" ISNULL OR
4269 NOT EXISTS (
4270 -- or no initiatives have been revoked lately
4271 SELECT NULL FROM "initiative"
4272 WHERE "issue_id" = "issue_id_p"
4273 AND now() < "revoked" + "issue_row"."verification_time"
4274 ) OR (
4275 -- or verification time has elapsed
4276 "issue_row"."half_frozen" NOTNULL AND
4277 "issue_row"."fully_frozen" ISNULL AND
4278 now() >= "issue_row"."half_frozen" + "issue_row"."verification_time"
4279 )
4280 )
4281 THEN
4282 -- NOTE: "issue_row" used later
4283 IF "issue_row"."accepted" ISNULL THEN
4284 "issue_row"."state" := 'canceled_revoked_before_accepted';
4285 ELSIF "issue_row"."half_frozen" ISNULL THEN
4286 "issue_row"."state" := 'canceled_after_revocation_during_discussion';
4287 ELSE
4288 "issue_row"."state" := 'canceled_after_revocation_during_verification';
4289 END IF;
4290 "issue_row"."closed" := now();
4291 UPDATE "issue" SET
4292 "state" = "issue_row"."state",
4293 "closed" = "issue_row"."closed"
4294 WHERE "id" = "issue_row"."id";
4295 END IF;
4296 -- fully freeze issue after verification time:
4297 IF
4298 "issue_row"."half_frozen" NOTNULL AND
4299 "issue_row"."fully_frozen" ISNULL AND
4300 "issue_row"."closed" ISNULL AND
4301 now() >= "issue_row"."half_frozen" + "issue_row"."verification_time"
4302 THEN
4303 PERFORM "freeze_after_snapshot"("issue_id_p");
4304 -- NOTE: "issue" might change, thus "issue_row" has to be updated below
4305 END IF;
4306 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
4307 -- close issue by calling close_voting(...) after voting time:
4308 IF
4309 "issue_row"."closed" ISNULL AND
4310 "issue_row"."fully_frozen" NOTNULL AND
4311 now() >= "issue_row"."fully_frozen" + "issue_row"."voting_time"
4312 THEN
4313 PERFORM "close_voting"("issue_id_p");
4314 -- calculate ranks will not consume much time and can be done now
4315 PERFORM "calculate_ranks"("issue_id_p");
4316 END IF;
4317 END IF;
4318 RETURN;
4319 END;
4320 $$;
4322 COMMENT ON FUNCTION "check_issue"
4323 ( "issue"."id"%TYPE )
4324 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.';
4327 CREATE FUNCTION "check_everything"()
4328 RETURNS VOID
4329 LANGUAGE 'plpgsql' VOLATILE AS $$
4330 DECLARE
4331 "issue_id_v" "issue"."id"%TYPE;
4332 BEGIN
4333 DELETE FROM "expired_session";
4334 PERFORM "check_activity"();
4335 PERFORM "calculate_member_counts"();
4336 FOR "issue_id_v" IN SELECT "id" FROM "open_issue" LOOP
4337 PERFORM "check_issue"("issue_id_v");
4338 END LOOP;
4339 FOR "issue_id_v" IN SELECT "id" FROM "issue_with_ranks_missing" LOOP
4340 PERFORM "calculate_ranks"("issue_id_v");
4341 END LOOP;
4342 RETURN;
4343 END;
4344 $$;
4346 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.';
4350 ----------------------
4351 -- Deletion of data --
4352 ----------------------
4355 CREATE FUNCTION "clean_issue"("issue_id_p" "issue"."id"%TYPE)
4356 RETURNS VOID
4357 LANGUAGE 'plpgsql' VOLATILE AS $$
4358 DECLARE
4359 "issue_row" "issue"%ROWTYPE;
4360 BEGIN
4361 SELECT * INTO "issue_row"
4362 FROM "issue" WHERE "id" = "issue_id_p"
4363 FOR UPDATE;
4364 IF "issue_row"."cleaned" ISNULL THEN
4365 UPDATE "issue" SET
4366 "state" = 'voting',
4367 "closed" = NULL,
4368 "ranks_available" = FALSE
4369 WHERE "id" = "issue_id_p";
4370 DELETE FROM "voting_comment"
4371 WHERE "issue_id" = "issue_id_p";
4372 DELETE FROM "delegating_voter"
4373 WHERE "issue_id" = "issue_id_p";
4374 DELETE FROM "direct_voter"
4375 WHERE "issue_id" = "issue_id_p";
4376 DELETE FROM "delegating_interest_snapshot"
4377 WHERE "issue_id" = "issue_id_p";
4378 DELETE FROM "direct_interest_snapshot"
4379 WHERE "issue_id" = "issue_id_p";
4380 DELETE FROM "delegating_population_snapshot"
4381 WHERE "issue_id" = "issue_id_p";
4382 DELETE FROM "direct_population_snapshot"
4383 WHERE "issue_id" = "issue_id_p";
4384 DELETE FROM "non_voter"
4385 WHERE "issue_id" = "issue_id_p";
4386 DELETE FROM "delegation"
4387 WHERE "issue_id" = "issue_id_p";
4388 DELETE FROM "supporter"
4389 WHERE "issue_id" = "issue_id_p";
4390 UPDATE "issue" SET
4391 "state" = "issue_row"."state",
4392 "closed" = "issue_row"."closed",
4393 "ranks_available" = "issue_row"."ranks_available",
4394 "cleaned" = now()
4395 WHERE "id" = "issue_id_p";
4396 END IF;
4397 RETURN;
4398 END;
4399 $$;
4401 COMMENT ON FUNCTION "clean_issue"("issue"."id"%TYPE) IS 'Delete discussion data and votes belonging to an issue';
4404 CREATE FUNCTION "delete_member"("member_id_p" "member"."id"%TYPE)
4405 RETURNS VOID
4406 LANGUAGE 'plpgsql' VOLATILE AS $$
4407 BEGIN
4408 UPDATE "member" SET
4409 "last_login" = NULL,
4410 "login" = NULL,
4411 "password" = NULL,
4412 "locked" = TRUE,
4413 "active" = FALSE,
4414 "notify_email" = NULL,
4415 "notify_email_unconfirmed" = NULL,
4416 "notify_email_secret" = NULL,
4417 "notify_email_secret_expiry" = NULL,
4418 "notify_email_lock_expiry" = NULL,
4419 "password_reset_secret" = NULL,
4420 "password_reset_secret_expiry" = NULL,
4421 "organizational_unit" = NULL,
4422 "internal_posts" = NULL,
4423 "realname" = NULL,
4424 "birthday" = NULL,
4425 "address" = NULL,
4426 "email" = NULL,
4427 "xmpp_address" = NULL,
4428 "website" = NULL,
4429 "phone" = NULL,
4430 "mobile_phone" = NULL,
4431 "profession" = NULL,
4432 "external_memberships" = NULL,
4433 "external_posts" = NULL,
4434 "statement" = NULL
4435 WHERE "id" = "member_id_p";
4436 -- "text_search_data" is updated by triggers
4437 DELETE FROM "setting" WHERE "member_id" = "member_id_p";
4438 DELETE FROM "setting_map" WHERE "member_id" = "member_id_p";
4439 DELETE FROM "member_relation_setting" WHERE "member_id" = "member_id_p";
4440 DELETE FROM "member_image" WHERE "member_id" = "member_id_p";
4441 DELETE FROM "contact" WHERE "member_id" = "member_id_p";
4442 DELETE FROM "ignored_member" WHERE "member_id" = "member_id_p";
4443 DELETE FROM "session" WHERE "member_id" = "member_id_p";
4444 DELETE FROM "area_setting" WHERE "member_id" = "member_id_p";
4445 DELETE FROM "issue_setting" WHERE "member_id" = "member_id_p";
4446 DELETE FROM "ignored_initiative" WHERE "member_id" = "member_id_p";
4447 DELETE FROM "initiative_setting" WHERE "member_id" = "member_id_p";
4448 DELETE FROM "suggestion_setting" WHERE "member_id" = "member_id_p";
4449 DELETE FROM "membership" WHERE "member_id" = "member_id_p";
4450 DELETE FROM "delegation" WHERE "truster_id" = "member_id_p";
4451 DELETE FROM "non_voter" WHERE "member_id" = "member_id_p";
4452 DELETE FROM "direct_voter" USING "issue"
4453 WHERE "direct_voter"."issue_id" = "issue"."id"
4454 AND "issue"."closed" ISNULL
4455 AND "member_id" = "member_id_p";
4456 RETURN;
4457 END;
4458 $$;
4460 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)';
4463 CREATE FUNCTION "delete_private_data"()
4464 RETURNS VOID
4465 LANGUAGE 'plpgsql' VOLATILE AS $$
4466 BEGIN
4467 DELETE FROM "member" WHERE "activated" ISNULL;
4468 UPDATE "member" SET
4469 "invite_code" = NULL,
4470 "invite_code_expiry" = NULL,
4471 "admin_comment" = NULL,
4472 "last_login" = NULL,
4473 "login" = NULL,
4474 "password" = NULL,
4475 "lang" = NULL,
4476 "notify_email" = NULL,
4477 "notify_email_unconfirmed" = NULL,
4478 "notify_email_secret" = NULL,
4479 "notify_email_secret_expiry" = NULL,
4480 "notify_email_lock_expiry" = NULL,
4481 "notify_level" = NULL,
4482 "password_reset_secret" = NULL,
4483 "password_reset_secret_expiry" = NULL,
4484 "organizational_unit" = NULL,
4485 "internal_posts" = NULL,
4486 "realname" = NULL,
4487 "birthday" = NULL,
4488 "address" = NULL,
4489 "email" = NULL,
4490 "xmpp_address" = NULL,
4491 "website" = NULL,
4492 "phone" = NULL,
4493 "mobile_phone" = NULL,
4494 "profession" = NULL,
4495 "external_memberships" = NULL,
4496 "external_posts" = NULL,
4497 "formatting_engine" = NULL,
4498 "statement" = NULL;
4499 -- "text_search_data" is updated by triggers
4500 DELETE FROM "setting";
4501 DELETE FROM "setting_map";
4502 DELETE FROM "member_relation_setting";
4503 DELETE FROM "member_image";
4504 DELETE FROM "contact";
4505 DELETE FROM "ignored_member";
4506 DELETE FROM "session";
4507 DELETE FROM "area_setting";
4508 DELETE FROM "issue_setting";
4509 DELETE FROM "ignored_initiative";
4510 DELETE FROM "initiative_setting";
4511 DELETE FROM "suggestion_setting";
4512 DELETE FROM "non_voter";
4513 DELETE FROM "direct_voter" USING "issue"
4514 WHERE "direct_voter"."issue_id" = "issue"."id"
4515 AND "issue"."closed" ISNULL;
4516 RETURN;
4517 END;
4518 $$;
4520 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. See source code to see which data is deleted. If you need a different behaviour, copy this function and modify lf_export accordingly, to avoid data-leaks after updating.';
4524 COMMIT;