liquid_feedback_core

changeset 532:5855ff9e5c8f

Several changes/additions for upcoming major release

- OAuth 2.0 support
- storing profiles as JSON document
- removed subject area membership
- revised snapshot system
- additional issue limiter (dynamic quorum in subject area)
- extended event logging in "event" table
author jbe
date Thu Mar 30 19:42:38 2017 +0200 (2017-03-30)
parents 37d6d15919f1
children d88fd3ae32d2
files core.sql lf_update.c test.sql update/core-update.v3.2.2-v4.0.0.sql
line diff
     1.1 --- a/core.sql	Sun Aug 21 17:31:44 2016 +0200
     1.2 +++ b/core.sql	Thu Mar 30 19:42:38 2017 +0200
     1.3 @@ -3,10 +3,10 @@
     1.4  
     1.5  BEGIN;
     1.6  
     1.7 -CREATE EXTENSION latlon;  -- load pgLatLon extenstion
     1.8 +CREATE EXTENSION IF NOT EXISTS latlon;  -- load pgLatLon extenstion
     1.9  
    1.10  CREATE VIEW "liquid_feedback_version" AS
    1.11 -  SELECT * FROM (VALUES ('4.0.0', 4, 0, 0))
    1.12 +  SELECT * FROM (VALUES ('4.0-dev', 4, 0, -1))
    1.13    AS "subquery"("string", "major", "minor", "revision");
    1.14  
    1.15  
    1.16 @@ -65,13 +65,15 @@
    1.17  
    1.18  
    1.19  CREATE TABLE "system_setting" (
    1.20 -        "member_ttl"            INTERVAL );
    1.21 +        "member_ttl"            INTERVAL,
    1.22 +        "snapshot_retention"    INTERVAL );
    1.23  CREATE UNIQUE INDEX "system_setting_singleton_idx" ON "system_setting" ((1));
    1.24  
    1.25  COMMENT ON TABLE "system_setting" IS 'This table contains only one row with different settings in each column.';
    1.26  COMMENT ON INDEX "system_setting_singleton_idx" IS 'This index ensures that "system_setting" only contains one row maximum.';
    1.27  
    1.28 -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.';
    1.29 +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.';
    1.30 +COMMENT ON COLUMN "system_setting"."snapshot_retention" IS 'Unreferenced snapshots are retained for the given period of time after creation; set to NULL for infinite retention.';
    1.31  
    1.32  
    1.33  CREATE TABLE "contingent" (
    1.34 @@ -124,22 +126,7 @@
    1.35          "name"                  TEXT            UNIQUE,
    1.36          "identification"        TEXT            UNIQUE,
    1.37          "authentication"        TEXT,
    1.38 -        "organizational_unit"   TEXT,
    1.39 -        "internal_posts"        TEXT,
    1.40 -        "realname"              TEXT,
    1.41 -        "birthday"              DATE,
    1.42 -        "address"               TEXT,
    1.43 -        "email"                 TEXT,
    1.44 -        "xmpp_address"          TEXT,
    1.45 -        "website"               TEXT,
    1.46 -        "phone"                 TEXT,
    1.47 -        "mobile_phone"          TEXT,
    1.48 -        "profession"            TEXT,
    1.49 -        "external_memberships"  TEXT,
    1.50 -        "external_posts"        TEXT,
    1.51 -        "formatting_engine"     TEXT,
    1.52 -        "statement"             TEXT,
    1.53 -        "location"              EPOINT,
    1.54 +        "location"              JSONB,
    1.55          "text_search_data"      TSVECTOR,
    1.56          CONSTRAINT "active_requires_activated_and_last_activity"
    1.57            CHECK ("active" = FALSE OR ("activated" NOTNULL AND "last_activity" NOTNULL)),
    1.58 @@ -155,14 +142,13 @@
    1.59            CHECK ("activated" ISNULL OR "name" NOTNULL) );
    1.60  CREATE INDEX "member_authority_login_idx" ON "member" ("authority_login");
    1.61  CREATE INDEX "member_active_idx" ON "member" ("active");
    1.62 -CREATE INDEX "member_location_idx" ON "member" USING gist ("location");
    1.63 +CREATE INDEX "member_location_idx" ON "member" USING gist ((GeoJSON_to_ecluster("location")));
    1.64  CREATE INDEX "member_text_search_data_idx" ON "member" USING gin ("text_search_data");
    1.65  CREATE TRIGGER "update_text_search_data"
    1.66    BEFORE INSERT OR UPDATE ON "member"
    1.67    FOR EACH ROW EXECUTE PROCEDURE
    1.68    tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
    1.69 -    "name", "identification", "organizational_unit", "internal_posts",
    1.70 -    "realname", "external_memberships", "external_posts", "statement" );
    1.71 +    "name", "identification");
    1.72  
    1.73  COMMENT ON TABLE "member" IS 'Users of the system, e.g. members of an organization';
    1.74  
    1.75 @@ -200,18 +186,10 @@
    1.76  COMMENT ON COLUMN "member"."name"                 IS 'Distinct name of the member, may be NULL if account has not been activated yet';
    1.77  COMMENT ON COLUMN "member"."identification"       IS 'Optional identification number or code of the member';
    1.78  COMMENT ON COLUMN "member"."authentication"       IS 'Information about how this member was authenticated';
    1.79 -COMMENT ON COLUMN "member"."organizational_unit"  IS 'Branch or division of the organization the member belongs to';
    1.80 -COMMENT ON COLUMN "member"."internal_posts"       IS 'Posts (offices) of the member inside the organization';
    1.81 -COMMENT ON COLUMN "member"."realname"             IS 'Real name of the member, may be identical with "name"';
    1.82 -COMMENT ON COLUMN "member"."email"                IS 'Published email address of the member; not used for system notifications';
    1.83 -COMMENT ON COLUMN "member"."external_memberships" IS 'Other organizations the member is involved in';
    1.84 -COMMENT ON COLUMN "member"."external_posts"       IS 'Posts (offices) outside the organization';
    1.85 -COMMENT ON COLUMN "member"."formatting_engine"    IS 'Allows different formatting engines (i.e. wiki formats) to be used for "member"."statement"';
    1.86 -COMMENT ON COLUMN "member"."statement"            IS 'Freely chosen text of the member for his/her profile';
    1.87 -COMMENT ON COLUMN "member"."location"             IS 'Geographic location on earth';
    1.88 -
    1.89 -
    1.90 -CREATE TABLE "member_history" (
    1.91 +COMMENT ON COLUMN "member"."location"             IS 'Geographic location on earth as GeoJSON object';
    1.92 +
    1.93 +
    1.94 +CREATE TABLE "member_history" (  -- TODO: redundancy with new "event" table
    1.95          "id"                    SERIAL8         PRIMARY KEY,
    1.96          "member_id"             INT4            NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
    1.97          "until"                 TIMESTAMPTZ     NOT NULL DEFAULT now(),
    1.98 @@ -225,6 +203,26 @@
    1.99  COMMENT ON COLUMN "member_history"."until" IS 'Timestamp until the data was valid';
   1.100  
   1.101  
   1.102 +CREATE TABLE "member_profile" (
   1.103 +        "member_id"             INT4            PRIMARY KEY REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.104 +        "formatting_engine"     TEXT,
   1.105 +        "statement"             TEXT,
   1.106 +        "profile"               JSONB,
   1.107 +        "profile_text_data"     TEXT,
   1.108 +        "text_search_data"      TSVECTOR );
   1.109 +CREATE INDEX "member_profile_text_search_data_idx" ON "member_profile" USING gin ("text_search_data");
   1.110 +CREATE TRIGGER "update_text_search_data"
   1.111 +  BEFORE INSERT OR UPDATE ON "member_profile"
   1.112 +  FOR EACH ROW EXECUTE PROCEDURE
   1.113 +  tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
   1.114 +    'statement', 'profile_text_data');
   1.115 +
   1.116 +COMMENT ON COLUMN "member_profile"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used for "member_profile"."statement"';
   1.117 +COMMENT ON COLUMN "member_profile"."statement"         IS 'Freely chosen text of the member for his/her profile';
   1.118 +COMMENT ON COLUMN "member_profile"."profile"           IS 'Additional profile data as JSON document';
   1.119 +COMMENT ON COLUMN "member_profile"."profile_text_data" IS 'Text data from "profile" field for full text search';
   1.120 +
   1.121 +
   1.122  CREATE TABLE "rendered_member_statement" (
   1.123          PRIMARY KEY ("member_id", "format"),
   1.124          "member_id"             INT4            REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.125 @@ -328,8 +326,11 @@
   1.126  
   1.127  
   1.128  CREATE TABLE "session" (
   1.129 -        "ident"                 TEXT            PRIMARY KEY,
   1.130 +        UNIQUE ("member_id", "id"),  -- index needed for foreign-key on table "token"
   1.131 +        "id"                    SERIAL8         PRIMARY KEY,
   1.132 +        "ident"                 TEXT            NOT NULL UNIQUE,
   1.133          "additional_secret"     TEXT,
   1.134 +        "logout_token"          TEXT,
   1.135          "expiry"                TIMESTAMPTZ     NOT NULL DEFAULT now() + '24 hours',
   1.136          "member_id"             INT4            REFERENCES "member" ("id") ON DELETE SET NULL,
   1.137          "authority"             TEXT,
   1.138 @@ -343,6 +344,7 @@
   1.139  
   1.140  COMMENT ON COLUMN "session"."ident"             IS 'Secret session identifier (i.e. random string)';
   1.141  COMMENT ON COLUMN "session"."additional_secret" IS 'Additional field to store a secret, which can be used against CSRF attacks';
   1.142 +COMMENT ON COLUMN "session"."logout_token"      IS 'Optional token to authorize logout through external component';
   1.143  COMMENT ON COLUMN "session"."member_id"         IS 'Reference to member, who is logged in';
   1.144  COMMENT ON COLUMN "session"."authority"         IS 'Temporary store for "member"."authority" during member account creation';
   1.145  COMMENT ON COLUMN "session"."authority_uid"     IS 'Temporary store for "member"."authority_uid" during member account creation';
   1.146 @@ -351,6 +353,136 @@
   1.147  COMMENT ON COLUMN "session"."lang"              IS 'Language code of the selected language';
   1.148  
   1.149  
   1.150 +CREATE TYPE "authflow" AS ENUM ('code', 'token');
   1.151 +
   1.152 +COMMENT ON TYPE "authflow" IS 'OAuth 2.0 flows: ''code'' = Authorization Code flow, ''token'' = Implicit flow';
   1.153 +
   1.154 +
   1.155 +CREATE TABLE "system_application" (
   1.156 +        "id"                    SERIAL4         PRIMARY KEY,
   1.157 +        "name"                  TEXT            NOT NULL,
   1.158 +        "client_id"             TEXT            NOT NULL UNIQUE,
   1.159 +        "default_redirect_uri"  TEXT            NOT NULL,
   1.160 +        "cert_common_name"      TEXT,
   1.161 +        "client_cred_scope"     TEXT,
   1.162 +        "flow"                  "authflow",
   1.163 +        "automatic_scope"       TEXT,
   1.164 +        "permitted_scope"       TEXT,
   1.165 +        "forbidden_scope"       TEXT );
   1.166 +
   1.167 +COMMENT ON TABLE "system_application" IS 'OAuth 2.0 clients that are registered by the system administrator';
   1.168 +
   1.169 +COMMENT ON COLUMN "system_application"."name"              IS 'Human readable name of application';
   1.170 +COMMENT ON COLUMN "system_application"."client_id"         IS 'OAuth 2.0 "client_id"';
   1.171 +COMMENT ON COLUMN "system_application"."cert_common_name"  IS 'Value for CN field of TLS client certificate';
   1.172 +COMMENT ON COLUMN "system_application"."client_cred_scope" IS 'Space-separated list of scopes; If set, Client Credentials Grant is allowed; value determines scope';
   1.173 +COMMENT ON COLUMN "system_application"."flow"              IS 'If set to ''code'' or ''token'', then Authorization Code or Implicit flow is allowed respectively';
   1.174 +COMMENT ON COLUMN "system_application"."automatic_scope"   IS 'Space-separated list of scopes; Automatically granted scope for Authorization Code or Implicit flow';
   1.175 +COMMENT ON COLUMN "system_application"."permitted_scope"   IS 'Space-separated list of scopes; If set, scope that members may grant to the application is limited to the given value';
   1.176 +COMMENT ON COLUMN "system_application"."forbidden_scope"   IS 'Space-separated list of scopes that may not be granted to the application by a member';
   1.177 +
   1.178 +
   1.179 +CREATE TABLE "system_application_redirect_uri" (
   1.180 +        PRIMARY KEY ("system_application_id", "redirect_uri"),
   1.181 +        "system_application_id" INT4            REFERENCES "system_application" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.182 +        "redirect_uri"          TEXT );
   1.183 +
   1.184 +COMMENT ON TABLE "system_application_redirect_uri" IS 'Additional OAuth 2.0 redirection endpoints, which may be selected through the "redirect_uri" GET parameter';
   1.185 +
   1.186 +
   1.187 +CREATE TABLE "dynamic_application_scope" (
   1.188 +        PRIMARY KEY ("redirect_uri", "flow", "scope"),
   1.189 +        "redirect_uri"          TEXT,
   1.190 +        "flow"                  TEXT,
   1.191 +        "scope"                 TEXT,
   1.192 +        "expiry"                TIMESTAMPTZ     NOT NULL DEFAULT now() + '24 hours' );
   1.193 +CREATE INDEX "dynamic_application_scope_redirect_uri_scope_idx" ON "dynamic_application_scope" ("redirect_uri", "flow", "scope");
   1.194 +CREATE INDEX "dynamic_application_scope_expiry_idx" ON "dynamic_application_scope" ("expiry");
   1.195 +
   1.196 +COMMENT ON TABLE "dynamic_application_scope" IS 'Dynamic OAuth 2.0 client registration data';
   1.197 +
   1.198 +COMMENT ON COLUMN "dynamic_application_scope"."redirect_uri" IS 'Redirection endpoint for which the registration has been done';
   1.199 +COMMENT ON COLUMN "dynamic_application_scope"."flow"         IS 'OAuth 2.0 flow for which the registration has been done (see also "system_application"."flow")';
   1.200 +COMMENT ON COLUMN "dynamic_application_scope"."scope"        IS 'Single scope without space characters (use multiple rows for more scopes)';
   1.201 +COMMENT ON COLUMN "dynamic_application_scope"."expiry"       IS 'Expiry unless renewed';
   1.202 +
   1.203 +
   1.204 +CREATE TABLE "member_application" (
   1.205 +        "id"                    SERIAL4         PRIMARY KEY,
   1.206 +        UNIQUE ("system_application_id", "member_id"),
   1.207 +        UNIQUE ("domain", "member_id"),
   1.208 +        "member_id"             INT4            NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.209 +        "system_application_id" INT4            REFERENCES "system_application" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.210 +        "domain"                TEXT,
   1.211 +        "session_id"            INT8,
   1.212 +        FOREIGN KEY ("member_id", "session_id") REFERENCES "session" ("member_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.213 +        "scope"                 TEXT            NOT NULL,
   1.214 +        CONSTRAINT "system_application_or_domain_but_not_both" CHECK (
   1.215 +          ("system_application_id" NOTNULL AND "domain" ISNULL) OR
   1.216 +          ("system_application_id" ISNULL AND "domain" NOTNULL) ) );
   1.217 +CREATE INDEX "member_application_member_id_idx" ON "member_application" ("member_id");
   1.218 +
   1.219 +COMMENT ON TABLE "member_application" IS 'Application authorized by a member';
   1.220 +
   1.221 +COMMENT ON COLUMN "member_application"."system_application_id" IS 'If set, then application is a system application';
   1.222 +COMMENT ON COLUMN "member_application"."domain"                IS 'If set, then application is a dynamically registered OAuth 2.0 client; value is set to client''s domain';
   1.223 +COMMENT ON COLUMN "member_application"."session_id"            IS 'If set, registration ends with session';
   1.224 +COMMENT ON COLUMN "member_application"."scope"                 IS 'Granted scope as space-separated list of strings';
   1.225 +
   1.226 +
   1.227 +CREATE TYPE "token_type" AS ENUM ('authorization', 'refresh', 'access');
   1.228 +
   1.229 +COMMENT ON TYPE "token_type" IS 'Types for entries in "token" table';
   1.230 +
   1.231 +
   1.232 +CREATE TABLE "token" (
   1.233 +        "id"                    SERIAL8         PRIMARY KEY,
   1.234 +        "token"                 TEXT            NOT NULL UNIQUE,
   1.235 +        "token_type"            "token_type"    NOT NULL,
   1.236 +        "authorization_token_id" INT8           REFERENCES "token" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.237 +        "member_id"             INT4            NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.238 +        "system_application_id" INT4            REFERENCES "system_application" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.239 +        "domain"                TEXT,
   1.240 +        FOREIGN KEY ("member_id", "domain") REFERENCES "member_application" ("member_id", "domain") ON DELETE CASCADE ON UPDATE CASCADE,
   1.241 +        "session_id"            INT8,
   1.242 +        FOREIGN KEY ("member_id", "session_id") REFERENCES "session" ("member_id", "id") ON DELETE RESTRICT ON UPDATE CASCADE,  -- NOTE: deletion through "detach_token_from_session" trigger on table "session"
   1.243 +        "redirect_uri"          TEXT,
   1.244 +        "redirect_uri_explicit" BOOLEAN,
   1.245 +        "created"               TIMESTAMPTZ     NOT NULL DEFAULT now(),
   1.246 +        "expiry"                TIMESTAMPTZ     DEFAULT now() + '1 hour',
   1.247 +        "used"                  BOOLEAN         NOT NULL DEFAULT FALSE,
   1.248 +        "scope"                 TEXT            NOT NULL,
   1.249 +        CONSTRAINT "access_token_needs_expiry"
   1.250 +          CHECK ("token_type" != 'access'::"token_type" OR "expiry" NOTNULL),
   1.251 +        CONSTRAINT "authorization_token_needs_redirect_uri"
   1.252 +          CHECK ("token_type" != 'authorization'::"token_type" OR ("redirect_uri" NOTNULL AND "redirect_uri_explicit" NOTNULL) ) );
   1.253 +CREATE INDEX "token_member_id_idx" ON "token" ("member_id");
   1.254 +CREATE INDEX "token_authorization_token_id_idx" ON "token" ("authorization_token_id");
   1.255 +CREATE INDEX "token_expiry_idx" ON "token" ("expiry");
   1.256 +
   1.257 +COMMENT ON TABLE "token" IS 'Issued OAuth 2.0 authorization codes and access/refresh tokens';
   1.258 +
   1.259 +COMMENT ON COLUMN "token"."token"                  IS 'String secret (the actual token)';
   1.260 +COMMENT ON COLUMN "token"."authorization_token_id" IS 'Reference to authorization token if tokens were originally created by Authorization Code flow (allows deletion if code is used twice)';
   1.261 +COMMENT ON COLUMN "token"."system_application_id"  IS 'If set, then application is a system application';
   1.262 +COMMENT ON COLUMN "token"."domain"                 IS 'If set, then application is a dynamically registered OAuth 2.0 client; value is set to client''s domain';
   1.263 +COMMENT ON COLUMN "token"."session_id"             IS 'If set, then token is tied to a session; Deletion of session sets value to NULL (via trigger) and removes all scopes without suffix ''_detached''';
   1.264 +COMMENT ON COLUMN "token"."redirect_uri"           IS 'Authorization codes must be bound to a specific redirect URI';
   1.265 +COMMENT ON COLUMN "token"."redirect_uri_explicit"  IS 'True if ''redirect_uri'' parameter was explicitly specified during authorization request of the Authorization Code flow (since RFC 6749 requires it to be included in the access token request in this case)';
   1.266 +COMMENT ON COLUMN "token"."expiry"                 IS 'Point in time when code or token expired; In case of "used" authorization codes, authorization code must not be deleted as long as tokens exist which refer to the authorization code';
   1.267 +COMMENT ON COLUMN "token"."used"                   IS 'Can be set to TRUE for authorization codes that have been used (enables deletion of authorization codes that were used twice)';
   1.268 +COMMENT ON COLUMN "token"."scope"                  IS 'Scope as space-separated list of strings (detached scopes are marked with ''_detached'' suffix)';
   1.269 +
   1.270 +
   1.271 +CREATE TABLE "token_scope" (
   1.272 +        PRIMARY KEY ("token_id", "index"),
   1.273 +        "token_id"              INT8            REFERENCES "token" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.274 +        "index"                 INT4,
   1.275 +        "scope"                 TEXT            NOT NULL );
   1.276 +
   1.277 +COMMENT ON TABLE "token_scope" IS 'Additional scopes for an authorization code if ''scope1'', ''scope2'', etc. parameters were used during Authorization Code flow to request several access and refresh tokens at once';
   1.278 +
   1.279 +
   1.280  CREATE TYPE "defeat_strength" AS ENUM ('simple', 'tuple');
   1.281  
   1.282  COMMENT ON TYPE "defeat_strength" IS 'How pairwise defeats are measured for the Schulze method: ''simple'' = only the number of winning votes, ''tuple'' = primarily the number of winning votes, secondarily the number of losing votes';
   1.283 @@ -373,7 +505,10 @@
   1.284          "discussion_time"       INTERVAL,
   1.285          "verification_time"     INTERVAL,
   1.286          "voting_time"           INTERVAL,
   1.287 -        "issue_quorum"          INT4            NOT NULL,
   1.288 +        "issue_quorum"          INT4            CHECK ("issue_quorum" >= 1),
   1.289 +        "issue_quorum_num"      INT4,
   1.290 +        "issue_quorum_den"      INT4,
   1.291 +        "initiative_quorum"     INT4            NOT NULL CHECK ("initiative_quorum" >= 1),
   1.292          "initiative_quorum_num" INT4            NOT NULL,
   1.293          "initiative_quorum_den" INT4            NOT NULL,
   1.294          "defeat_strength"     "defeat_strength" NOT NULL DEFAULT 'tuple',
   1.295 @@ -391,7 +526,9 @@
   1.296          "no_reverse_beat_path"          BOOLEAN NOT NULL DEFAULT FALSE,
   1.297          "no_multistage_majority"        BOOLEAN NOT NULL DEFAULT FALSE,
   1.298          CONSTRAINT "issue_quorum_if_and_only_if_not_polling" CHECK (
   1.299 -          "polling" = ("issue_quorum" ISNULL) ),
   1.300 +          "polling" = ("issue_quorum"     ISNULL) AND
   1.301 +          "polling" = ("issue_quorum_num" ISNULL) AND
   1.302 +          "polling" = ("issue_quorum_den" ISNULL) ),
   1.303          CONSTRAINT "min_admission_time_smaller_than_max_admission_time" CHECK (
   1.304            "min_admission_time" < "max_admission_time" ),
   1.305          CONSTRAINT "timing_null_or_not_null_constraints" CHECK (
   1.306 @@ -425,7 +562,10 @@
   1.307  COMMENT ON COLUMN "policy"."discussion_time"       IS 'Duration of issue state ''discussion''; Regular time until an issue is "half_frozen" after being "accepted"';
   1.308  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"';
   1.309  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'')';
   1.310 -COMMENT ON COLUMN "policy"."issue_quorum"          IS 'Minimum number of supporters needed for one initiative of an issue to allow the issue to pass from ''admission'' to ''discussion'' state (Note: further requirements apply, see tables "admission_rule" and "admission_rule_condition")';
   1.311 +COMMENT ON COLUMN "policy"."issue_quorum"          IS 'Absolute number of supporters needed by an initiative to be "accepted", i.e. pass from ''admission'' to ''discussion'' state';
   1.312 +COMMENT ON COLUMN "policy"."issue_quorum_num"      IS 'Numerator of supporter quorum to be reached by an initiative to be "accepted", i.e. pass from ''admission'' to ''discussion'' state (Note: further requirements apply, see quorum columns of "area" table)';
   1.313 +COMMENT ON COLUMN "policy"."issue_quorum_den"      IS 'Denominator of supporter quorum to be reached by an initiative to be "accepted", i.e. pass from ''admission'' to ''discussion'' state (Note: further requirements apply, see quorum columns of "area" table)';
   1.314 +COMMENT ON COLUMN "policy"."initiative_quorum"     IS 'Absolute number of satisfied supporters to be reached by an initiative to be "admitted" for voting';
   1.315  COMMENT ON COLUMN "policy"."initiative_quorum_num" IS 'Numerator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
   1.316  COMMENT ON COLUMN "policy"."initiative_quorum_den" IS 'Denominator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
   1.317  COMMENT ON COLUMN "policy"."defeat_strength"       IS 'How pairwise defeats are measured for the Schulze method; see type "defeat_strength"; ''tuple'' is the recommended setting';
   1.318 @@ -452,12 +592,12 @@
   1.319          "description"           TEXT            NOT NULL DEFAULT '',
   1.320          "external_reference"    TEXT,
   1.321          "member_count"          INT4,
   1.322 -        "region"                ECLUSTER,
   1.323 +        "region"                JSONB,
   1.324          "text_search_data"      TSVECTOR );
   1.325  CREATE INDEX "unit_root_idx" ON "unit" ("id") WHERE "parent_id" ISNULL;
   1.326  CREATE INDEX "unit_parent_id_idx" ON "unit" ("parent_id");
   1.327  CREATE INDEX "unit_active_idx" ON "unit" ("active");
   1.328 -CREATE INDEX "unit_region_idx" ON "unit" USING gist ("region");
   1.329 +CREATE INDEX "unit_region_idx" ON "unit" USING gist ((GeoJSON_to_ecluster("region")));
   1.330  CREATE INDEX "unit_text_search_data_idx" ON "unit" USING gin ("text_search_data");
   1.331  CREATE TRIGGER "update_text_search_data"
   1.332    BEFORE INSERT OR UPDATE ON "unit"
   1.333 @@ -494,18 +634,24 @@
   1.334  
   1.335  
   1.336  CREATE TABLE "area" (
   1.337 -        UNIQUE ("unit_id", "id"),  -- index needed for foreign-key on table "admission_rule_condition"
   1.338 +        UNIQUE ("unit_id", "id"),  -- index needed for foreign-key on table "event"
   1.339 +        "id"                    SERIAL4         PRIMARY KEY,
   1.340          "unit_id"               INT4            NOT NULL REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.341 -        "id"                    SERIAL4         PRIMARY KEY,
   1.342          "active"                BOOLEAN         NOT NULL DEFAULT TRUE,
   1.343          "name"                  TEXT            NOT NULL,
   1.344          "description"           TEXT            NOT NULL DEFAULT '',
   1.345 +        "quorum_standard"       NUMERIC         NOT NULL DEFAULT 2 CHECK ("quorum_standard" >= 0),
   1.346 +        "quorum_issues"         NUMERIC         NOT NULL DEFAULT 1 CHECK ("quorum_issues" > 0),
   1.347 +        "quorum_time"           INTERVAL        NOT NULL DEFAULT '1 day' CHECK ("quorum_time" > '0'::INTERVAL),
   1.348 +        "quorum_exponent"       NUMERIC         NOT NULL DEFAULT 0.5 CHECK ("quorum_exponent" BETWEEN 0 AND 1),
   1.349 +        "quorum_factor"         NUMERIC         NOT NULL DEFAULT 2 CHECK ("quorum_factor" >= 1),
   1.350 +        "quorum_den"            INT4            CHECK ("quorum_den" > 0),
   1.351 +        "issue_quorum"          INT4,
   1.352          "external_reference"    TEXT,
   1.353 -        "region"                ECLUSTER,
   1.354 +        "region"                JSONB,
   1.355          "text_search_data"      TSVECTOR );
   1.356 -CREATE INDEX "area_unit_id_idx" ON "area" ("unit_id");
   1.357  CREATE INDEX "area_active_idx" ON "area" ("active");
   1.358 -CREATE INDEX "area_region_idx" ON "area" USING gist ("region");
   1.359 +CREATE INDEX "area_region_idx" ON "area" USING gist ((GeoJSON_to_ecluster("region")));
   1.360  CREATE INDEX "area_text_search_data_idx" ON "area" USING gin ("text_search_data");
   1.361  CREATE TRIGGER "update_text_search_data"
   1.362    BEFORE INSERT OR UPDATE ON "area"
   1.363 @@ -516,6 +662,13 @@
   1.364  COMMENT ON TABLE "area" IS 'Subject areas';
   1.365  
   1.366  COMMENT ON COLUMN "area"."active"             IS 'TRUE means new issues can be created in this area';
   1.367 +COMMENT ON COLUMN "area"."quorum_standard"    IS 'Parameter for dynamic issue quorum: default quorum';
   1.368 +COMMENT ON COLUMN "area"."quorum_issues"      IS 'Parameter for dynamic issue quorum: number of open issues for default quorum';
   1.369 +COMMENT ON COLUMN "area"."quorum_time"        IS 'Parameter for dynamic issue quorum: discussion, verification, and voting time of open issues to result in the given default quorum (open issues with shorter time will increase quorum and open issues with longer time will reduce quorum if "quorum_exponent" is greater than zero)';
   1.370 +COMMENT ON COLUMN "area"."quorum_exponent"    IS 'Parameter for dynamic issue quorum: set to zero to ignore duration of open issues, set to one to fully take duration of open issues into account; defaults to 0.5';
   1.371 +COMMENT ON COLUMN "area"."quorum_factor"      IS 'Parameter for dynamic issue quorum: factor to increase dynamic quorum when a number of "quorum_issues" issues with "quorum_time" duration of discussion, verification, and voting phase are added to the number of open admitted issues';
   1.372 +COMMENT ON COLUMN "area"."quorum_den"         IS 'Parameter for dynamic issue quorum: when set, dynamic quorum is multiplied with "issue"."population" and divided by "quorum_den" (and then rounded up)';
   1.373 +COMMENT ON COLUMN "area"."issue_quorum"       IS 'Additional dynamic issue quorum based on the number of open accepted issues; automatically calculated by function "issue_admission"';
   1.374  COMMENT ON COLUMN "area"."external_reference" IS 'Opaque data field to store an external reference';
   1.375  COMMENT ON COLUMN "area"."region"             IS 'Scattered (or hollow) polygon represented as an array of polygons indicating valid coordinates for initiatives of issues with this policy';
   1.376  
   1.377 @@ -551,45 +704,25 @@
   1.378  COMMENT ON COLUMN "allowed_policy"."default_policy" IS 'One policy per area can be set as default.';
   1.379  
   1.380  
   1.381 -CREATE TABLE "admission_rule" (
   1.382 -        UNIQUE ("unit_id", "id"),  -- index needed for foreign-key on table "admission_rule_condition"
   1.383 -        "unit_id"               INT4            NOT NULL REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.384 -        "id"                    SERIAL4         PRIMARY KEY,
   1.385 -        "name"                  TEXT            NOT NULL,
   1.386 -        "description"           TEXT            NOT NULL DEFAULT '' );
   1.387 -
   1.388 -COMMENT ON TABLE "admission_rule" IS 'Groups entries in "admission_rule_condition" to regulate how many issues may pass from ''admission'' to ''discussion'' state in a given time';
   1.389 -
   1.390 -
   1.391 -CREATE TABLE "admission_rule_condition" (
   1.392 -        "unit_id"               INT4            NOT NULL,
   1.393 -        "admission_rule_id"     INT4,
   1.394 -        FOREIGN KEY ("unit_id", "admission_rule_id") REFERENCES "admission_rule" ("unit_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.395 -        "policy_id"             INT4            REFERENCES "policy" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.396 -        "area_id"               INT4,
   1.397 -        FOREIGN KEY ("unit_id", "area_id") REFERENCES "area" ("unit_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.398 -        "holdoff_time"          INTERVAL        NOT NULL );
   1.399 -CREATE UNIQUE INDEX "admission_rule_condition_unit_idx" ON "admission_rule_condition" ("admission_rule_id") WHERE "policy_id" ISNULL AND "area_id" ISNULL;
   1.400 -CREATE UNIQUE INDEX "admission_rule_condition_policy_idx" ON "admission_rule_condition" ("admission_rule_id", "policy_id") WHERE "area_id" ISNULL;
   1.401 -CREATE UNIQUE INDEX "admission_rule_condition_area_idx" ON "admission_rule_condition" ("admission_rule_id", "area_id") WHERE "policy_id" ISNULL;
   1.402 -CREATE UNIQUE INDEX "admission_rule_condition_policy_area_idx" ON "admission_rule_condition" ("admission_rule_id", "policy_id", "area_id");
   1.403 -
   1.404 -COMMENT ON TABLE "admission_rule_condition" IS 'Regulates how many issues may pass from ''admission'' to ''discussion'' state in a given time; See definition of "issue_for_admission" view for details';
   1.405 -
   1.406 -COMMENT ON COLUMN "admission_rule_condition"."unit_id"           IS 'Grouped "admission_rule_condition" rows must have the same "unit_id"';
   1.407 -COMMENT ON COLUMN "admission_rule_condition"."admission_rule_id" IS 'Grouping several "admission_rule_condition" rows';
   1.408 -COMMENT ON COLUMN "admission_rule_condition"."policy_id"         IS 'Set to link the condition with a given policy, NULL for any policy in the issue';
   1.409 -COMMENT ON COLUMN "admission_rule_condition"."area_id"           IS 'Set to link the condition with a given policy, NULL for any area in the issue';
   1.410 -COMMENT ON COLUMN "admission_rule_condition"."holdoff_time"      IS 'After an issue in the given unit, policy, and/or area has been admitted, the "admission_rule" is disabled for the selected "holdoff_time", e.g. a "holdoff_time" of ''6 hours'' causes four issues per day to be admitted';
   1.411 -
   1.412 -
   1.413  CREATE TABLE "snapshot" (
   1.414 +        UNIQUE ("issue_id", "id"),  -- index needed for foreign-key on table "issue"
   1.415          "id"                    SERIAL8         PRIMARY KEY,
   1.416 -        "calculated"            TIMESTAMPTZ     NOT NULL DEFAULT now() );
   1.417 +        "calculated"            TIMESTAMPTZ     NOT NULL DEFAULT now(),
   1.418 +        "population"            INT4,
   1.419 +        "area_id"               INT4            NOT NULL REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.420 +        "issue_id"              INT4 );         -- NOTE: following (cyclic) reference is added later through ALTER command: REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE
   1.421  
   1.422  COMMENT ON TABLE "snapshot" IS 'Point in time when a snapshot of one or more issues (see table "snapshot_issue") and their supporter situation is taken';
   1.423  
   1.424  
   1.425 +CREATE TABLE "snapshot_population" (
   1.426 +        PRIMARY KEY ("snapshot_id", "member_id"),
   1.427 +        "snapshot_id"           INT8            REFERENCES "snapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.428 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE );
   1.429 +
   1.430 +COMMENT ON TABLE "snapshot_population" IS 'Members with voting right relevant for a snapshot';
   1.431 +
   1.432 +
   1.433  CREATE TYPE "issue_state" AS ENUM (
   1.434          'admission', 'discussion', 'verification', 'voting',
   1.435          'canceled_by_admin',
   1.436 @@ -604,6 +737,7 @@
   1.437  
   1.438  
   1.439  CREATE TABLE "issue" (
   1.440 +        UNIQUE ("area_id", "id"),  -- index needed for foreign-key on table "event"
   1.441          "id"                    SERIAL4         PRIMARY KEY,
   1.442          "area_id"               INT4            NOT NULL REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.443          "policy_id"             INT4            NOT NULL REFERENCES "policy" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.444 @@ -622,10 +756,15 @@
   1.445          "discussion_time"       INTERVAL        NOT NULL,
   1.446          "verification_time"     INTERVAL        NOT NULL,
   1.447          "voting_time"           INTERVAL        NOT NULL,
   1.448 +        "calculated"            TIMESTAMPTZ,  -- NOTE: copy of "calculated" column of latest snapshot, but no referential integrity to avoid overhead
   1.449          "latest_snapshot_id"    INT8            REFERENCES "snapshot" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.450          "admission_snapshot_id" INT8            REFERENCES "snapshot" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
   1.451 -        "half_freeze_snapshot_id" INT8          REFERENCES "snapshot" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.452 -        "full_freeze_snapshot_id" INT8          REFERENCES "snapshot" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.453 +        "half_freeze_snapshot_id" INT8,
   1.454 +        FOREIGN KEY ("id", "half_freeze_snapshot_id")
   1.455 +          REFERENCES "snapshot" ("issue_id", "id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.456 +        "full_freeze_snapshot_id" INT8,
   1.457 +        FOREIGN KEY ("id", "full_freeze_snapshot_id")
   1.458 +          REFERENCES "snapshot" ("issue_id", "id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.459          "population"            INT4,
   1.460          "voter_count"           INT4,
   1.461          "status_quo_schulze_rank" INT4,
   1.462 @@ -665,7 +804,6 @@
   1.463            --("accepted" ISNULL OR "admission_snapshot_id" NOTNULL) AND
   1.464            ("half_frozen" ISNULL OR "half_freeze_snapshot_id" NOTNULL) AND
   1.465            ("fully_frozen" ISNULL OR "full_freeze_snapshot_id" NOTNULL) ) );
   1.466 -CREATE INDEX "issue_area_id_idx" ON "issue" ("area_id");
   1.467  CREATE INDEX "issue_policy_id_idx" ON "issue" ("policy_id");
   1.468  CREATE INDEX "issue_state_idx" ON "issue" ("state");
   1.469  CREATE INDEX "issue_created_idx" ON "issue" ("created");
   1.470 @@ -685,7 +823,7 @@
   1.471  COMMENT ON COLUMN "issue"."admin_notice"            IS 'Public notice by admin to explain manual interventions, or to announce corrections';
   1.472  COMMENT ON COLUMN "issue"."external_reference"      IS 'Opaque data field to store an external reference';
   1.473  COMMENT ON COLUMN "issue"."phase_finished"          IS 'Set to a value NOTNULL, if the current phase has finished, but calculations are pending; No changes in this issue shall be made by the frontend or API when this value is set';
   1.474 -COMMENT ON COLUMN "issue"."accepted"                IS 'Point in time, when the issue was accepted for further discussion (see table "admission_rule" and column "issue_quorum" of table "policy")';
   1.475 +COMMENT ON COLUMN "issue"."accepted"                IS 'Point in time, when the issue was accepted for further discussion (see columns "issue_quorum_num" and "issue_quorum_den" of table "policy" and quorum columns of table "area")';
   1.476  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.';
   1.477  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.';
   1.478  COMMENT ON COLUMN "issue"."closed"                  IS 'Point in time, when "max_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.';
   1.479 @@ -695,17 +833,21 @@
   1.480  COMMENT ON COLUMN "issue"."discussion_time"         IS 'Copied from "policy" table at creation of issue';
   1.481  COMMENT ON COLUMN "issue"."verification_time"       IS 'Copied from "policy" table at creation of issue';
   1.482  COMMENT ON COLUMN "issue"."voting_time"             IS 'Copied from "policy" table at creation of issue';
   1.483 +COMMENT ON COLUMN "issue"."calculated"              IS 'Point in time, when most recent snapshot and "population" and *_count values were calculated (NOTE: value is equal to "snapshot"."calculated" of snapshot with "id"="issue"."latest_snapshot_id")';
   1.484  COMMENT ON COLUMN "issue"."latest_snapshot_id"      IS 'Snapshot id of most recent snapshot';
   1.485  COMMENT ON COLUMN "issue"."admission_snapshot_id"   IS 'Snapshot id when issue as accepted or canceled in admission phase';
   1.486  COMMENT ON COLUMN "issue"."half_freeze_snapshot_id" IS 'Snapshot id at end of discussion phase';
   1.487  COMMENT ON COLUMN "issue"."full_freeze_snapshot_id" IS 'Snapshot id at end of verification phase';
   1.488 -COMMENT ON COLUMN "issue"."population"              IS 'Sum of "weight" column in table "direct_population_snapshot"';
   1.489 +COMMENT ON COLUMN "issue"."population"              IS 'Count of members in "snapshot_population" table with "snapshot_id" equal to "issue"."latest_snapshot_id"';
   1.490  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';
   1.491  COMMENT ON COLUMN "issue"."status_quo_schulze_rank" IS 'Schulze rank of status quo, as calculated by "calculate_ranks" function';
   1.492  
   1.493  
   1.494 +ALTER TABLE "snapshot" ADD FOREIGN KEY ("issue_id") REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
   1.495 +
   1.496 +
   1.497  CREATE TABLE "issue_order_in_admission_state" (
   1.498 -        "id"                    INT8            PRIMARY KEY, --REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.499 +        "id"                    INT8            PRIMARY KEY, -- NOTE: no referential integrity due to performans/locking issues; REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.500          "order_in_area"         INT4,
   1.501          "order_in_unit"         INT4 );
   1.502  
   1.503 @@ -735,9 +877,8 @@
   1.504          "created"               TIMESTAMPTZ     NOT NULL DEFAULT now(),
   1.505          "revoked"               TIMESTAMPTZ,
   1.506          "revoked_by_member_id"  INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.507 -        "suggested_initiative_id" INT4          REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.508 -        "location1"             EPOINT,
   1.509 -        "location2"             EPOINT,
   1.510 +        "suggested_initiative_id" INT4          REFERENCES "initiative" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
   1.511 +        "location"              JSONB,
   1.512          "external_reference"    TEXT,
   1.513          "admitted"              BOOLEAN,
   1.514          "supporter_count"                    INT4,
   1.515 @@ -765,8 +906,6 @@
   1.516            CHECK (("revoked" NOTNULL) = ("revoked_by_member_id" NOTNULL)),
   1.517          CONSTRAINT "non_revoked_initiatives_cant_suggest_other"
   1.518            CHECK ("revoked" NOTNULL OR "suggested_initiative_id" ISNULL),
   1.519 -        CONSTRAINT "location2_requires_location1"
   1.520 -          CHECK ("location2" ISNULL OR "location1" NOTNULL),
   1.521          CONSTRAINT "revoked_initiatives_cant_be_admitted"
   1.522            CHECK ("revoked" ISNULL OR "admitted" ISNULL),
   1.523          CONSTRAINT "non_admitted_initiatives_cant_contain_voting_results" CHECK (
   1.524 @@ -788,8 +927,7 @@
   1.525          CONSTRAINT "unique_rank_per_issue" UNIQUE ("issue_id", "rank") );
   1.526  CREATE INDEX "initiative_created_idx" ON "initiative" ("created");
   1.527  CREATE INDEX "initiative_revoked_idx" ON "initiative" ("revoked");
   1.528 -CREATE INDEX "initiative_location1_idx" ON "initiative" USING gist ("location1");
   1.529 -CREATE INDEX "initiative_location2_idx" ON "initiative" USING gist ("location2");
   1.530 +CREATE INDEX "initiative_location_idx" ON "initiative" USING gist ((GeoJSON_to_ecluster("location")));
   1.531  CREATE INDEX "initiative_text_search_data_idx" ON "initiative" USING gin ("text_search_data");
   1.532  CREATE INDEX "initiative_draft_text_search_data_idx" ON "initiative" USING gin ("draft_text_search_data");
   1.533  CREATE TRIGGER "update_text_search_data"
   1.534 @@ -802,8 +940,7 @@
   1.535  COMMENT ON COLUMN "initiative"."polling"                IS 'Initiative does not need to pass the initiative quorum (see "policy"."polling")';
   1.536  COMMENT ON COLUMN "initiative"."revoked"                IS 'Point in time, when one initiator decided to revoke the initiative';
   1.537  COMMENT ON COLUMN "initiative"."revoked_by_member_id"   IS 'Member, who decided to revoke the initiative';
   1.538 -COMMENT ON COLUMN "initiative"."location1"              IS 'Geographic location of initiative (automatically copied from most recent draft)';
   1.539 -COMMENT ON COLUMN "initiative"."location2"              IS 'Geographic location of initiative''s second marker (automatically copied from most recent draft)';
   1.540 +COMMENT ON COLUMN "initiative"."location"               IS 'Geographic location of initiative as GeoJSON object (automatically copied from most recent draft)';
   1.541  COMMENT ON COLUMN "initiative"."external_reference"     IS 'Opaque data field to store an external reference';
   1.542  COMMENT ON COLUMN "initiative"."admitted"               IS 'TRUE, if initiative reaches the "initiative_quorum" when freezing the issue';
   1.543  COMMENT ON COLUMN "initiative"."supporter_count"                    IS 'Calculated from table "direct_supporter_snapshot"';
   1.544 @@ -872,16 +1009,12 @@
   1.545          "author_id"             INT4            NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.546          "formatting_engine"     TEXT,
   1.547          "content"               TEXT            NOT NULL,
   1.548 -        "location1"             EPOINT,
   1.549 -        "location2"             EPOINT,
   1.550 +        "location"              JSONB,
   1.551          "external_reference"    TEXT,
   1.552 -        "text_search_data"      TSVECTOR,
   1.553 -        CONSTRAINT "location2_requires_location1"
   1.554 -          CHECK ("location2" ISNULL OR "location1" NOTNULL) );
   1.555 +        "text_search_data"      TSVECTOR );
   1.556  CREATE INDEX "draft_created_idx" ON "draft" ("created");
   1.557  CREATE INDEX "draft_author_id_created_idx" ON "draft" ("author_id", "created");
   1.558 -CREATE INDEX "draft_location1_idx" ON "draft" USING gist ("location1");
   1.559 -CREATE INDEX "draft_location2_idx" ON "draft" USING gist ("location2");
   1.560 +CREATE INDEX "draft_location_idx" ON "draft" USING gist ((GeoJSON_to_ecluster("location")));
   1.561  CREATE INDEX "draft_text_search_data_idx" ON "draft" USING gin ("text_search_data");
   1.562  CREATE TRIGGER "update_text_search_data"
   1.563    BEFORE INSERT OR UPDATE ON "draft"
   1.564 @@ -892,8 +1025,7 @@
   1.565  
   1.566  COMMENT ON COLUMN "draft"."formatting_engine"  IS 'Allows different formatting engines (i.e. wiki formats) to be used';
   1.567  COMMENT ON COLUMN "draft"."content"            IS 'Text of the draft in a format depending on the field "formatting_engine"';
   1.568 -COMMENT ON COLUMN "draft"."location1"          IS 'Geographic location of initiative (automatically copied to "initiative" table if draft is most recent)';
   1.569 -COMMENT ON COLUMN "draft"."location2"          IS 'Geographic location of initiative''s second marker (automatically copied to "initiative" table if draft is most recent)';
   1.570 +COMMENT ON COLUMN "draft"."location"           IS 'Geographic location of initiative as GeoJSON object (automatically copied to "initiative" table if draft is most recent)';
   1.571  COMMENT ON COLUMN "draft"."external_reference" IS 'Opaque data field to store an external reference';
   1.572  
   1.573  
   1.574 @@ -917,8 +1049,7 @@
   1.575          "name"                  TEXT            NOT NULL,
   1.576          "formatting_engine"     TEXT,
   1.577          "content"               TEXT            NOT NULL DEFAULT '',
   1.578 -        "location1"             EPOINT,
   1.579 -        "location2"             EPOINT,
   1.580 +        "location"              JSONB,
   1.581          "external_reference"    TEXT,
   1.582          "text_search_data"      TSVECTOR,
   1.583          "minus2_unfulfilled_count" INT4,
   1.584 @@ -929,13 +1060,10 @@
   1.585          "plus1_fulfilled_count"    INT4,
   1.586          "plus2_unfulfilled_count"  INT4,
   1.587          "plus2_fulfilled_count"    INT4,
   1.588 -        "proportional_order"    INT4,
   1.589 -        CONSTRAINT "location2_requires_location1"
   1.590 -          CHECK ("location2" ISNULL OR "location1" NOTNULL) );
   1.591 +        "proportional_order"    INT4 );
   1.592  CREATE INDEX "suggestion_created_idx" ON "suggestion" ("created");
   1.593  CREATE INDEX "suggestion_author_id_created_idx" ON "suggestion" ("author_id", "created");
   1.594 -CREATE INDEX "suggestion_location1_idx" ON "suggestion" USING gist ("location1");
   1.595 -CREATE INDEX "suggestion_location2_idx" ON "suggestion" USING gist ("location2");
   1.596 +CREATE INDEX "suggestion_location_idx" ON "suggestion" USING gist ((GeoJSON_to_ecluster("location")));
   1.597  CREATE INDEX "suggestion_text_search_data_idx" ON "suggestion" USING gin ("text_search_data");
   1.598  CREATE TRIGGER "update_text_search_data"
   1.599    BEFORE INSERT OR UPDATE ON "suggestion"
   1.600 @@ -946,8 +1074,7 @@
   1.601  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';
   1.602  
   1.603  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")';
   1.604 -COMMENT ON COLUMN "suggestion"."location1"                IS 'Geographic location of suggestion';
   1.605 -COMMENT ON COLUMN "suggestion"."location2"                IS 'Geographic location of suggestion''s second marker';
   1.606 +COMMENT ON COLUMN "suggestion"."location"                 IS 'Geographic location of suggestion as GeoJSON object';
   1.607  COMMENT ON COLUMN "suggestion"."external_reference"       IS 'Opaque data field to store an external reference';
   1.608  COMMENT ON COLUMN "suggestion"."minus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
   1.609  COMMENT ON COLUMN "suggestion"."minus2_fulfilled_count"   IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
   1.610 @@ -980,7 +1107,7 @@
   1.611  
   1.612  
   1.613  CREATE TABLE "temporary_suggestion_counts" (
   1.614 -        "id"                    INT8            PRIMARY KEY, --REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.615 +        "id"                    INT8            PRIMARY KEY, -- NOTE: no referential integrity due to performance/locking issues; REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.616          "minus2_unfulfilled_count" INT4         NOT NULL,
   1.617          "minus2_fulfilled_count"   INT4         NOT NULL,
   1.618          "minus1_unfulfilled_count" INT4         NOT NULL,
   1.619 @@ -1021,7 +1148,7 @@
   1.620  CREATE TABLE "interest" (
   1.621          PRIMARY KEY ("issue_id", "member_id"),
   1.622          "issue_id"              INT4            REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.623 -        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
   1.624 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE );
   1.625  CREATE INDEX "interest_member_id_idx" ON "interest" ("member_id");
   1.626  
   1.627  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.';
   1.628 @@ -1030,7 +1157,7 @@
   1.629  CREATE TABLE "initiator" (
   1.630          PRIMARY KEY ("initiative_id", "member_id"),
   1.631          "initiative_id"         INT4            REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.632 -        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.633 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.634          "accepted"              BOOLEAN );
   1.635  CREATE INDEX "initiator_member_id_idx" ON "initiator" ("member_id");
   1.636  
   1.637 @@ -1079,7 +1206,7 @@
   1.638  CREATE TABLE "delegation" (
   1.639          "id"                    SERIAL8         PRIMARY KEY,
   1.640          "truster_id"            INT4            NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.641 -        "trustee_id"            INT4            REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.642 +        "trustee_id"            INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.643          "scope"              "delegation_scope" NOT NULL,
   1.644          "unit_id"               INT4            REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.645          "area_id"               INT4            REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.646 @@ -1107,11 +1234,13 @@
   1.647  CREATE TABLE "snapshot_issue" (
   1.648          PRIMARY KEY ("snapshot_id", "issue_id"),
   1.649          "snapshot_id"           INT8            REFERENCES "snapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.650 -        "issue_id"              INT4            REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
   1.651 +        "issue_id"              INT4            REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE );  -- NOTE: trigger "delete_snapshot_on_partial_delete" will delete whole "snapshot"
   1.652  CREATE INDEX "snapshot_issue_issue_id_idx" ON "snapshot_issue" ("issue_id");
   1.653  
   1.654  COMMENT ON TABLE "snapshot_issue" IS 'List of issues included in a snapshot';
   1.655  
   1.656 +COMMENT ON COLUMN "snapshot_issue"."issue_id" IS 'Issue being part of the snapshot; Trigger "delete_snapshot_on_partial_delete" on "snapshot_issue" table will delete snapshot if an issue of the snapshot is deleted.';
   1.657 +
   1.658  
   1.659  CREATE TABLE "direct_interest_snapshot" (
   1.660          PRIMARY KEY ("snapshot_id", "issue_id", "member_id"),
   1.661 @@ -1258,7 +1387,21 @@
   1.662          'initiative_created_in_existing_issue',
   1.663          'initiative_revoked',
   1.664          'new_draft_created',
   1.665 -        'suggestion_created');
   1.666 +        'suggestion_created',
   1.667 +        'suggestion_removed',
   1.668 +        'member_activated',
   1.669 +        'member_removed',
   1.670 +        'member_active',
   1.671 +        'member_name_updated',
   1.672 +        'member_profile_updated',
   1.673 +        'member_image_updated',
   1.674 +        'interest',
   1.675 +        'initiator',
   1.676 +        'support',
   1.677 +        'support_updated',
   1.678 +        'suggestion_rated',
   1.679 +        'delegation',
   1.680 +        'contact' );
   1.681  
   1.682  COMMENT ON TYPE "event_type" IS 'Type used for column "event" of table "event"';
   1.683  
   1.684 @@ -1268,49 +1411,267 @@
   1.685          "occurrence"            TIMESTAMPTZ     NOT NULL DEFAULT now(),
   1.686          "event"                 "event_type"    NOT NULL,
   1.687          "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.688 +        "other_member_id"       INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
   1.689 +        "scope"                 "delegation_scope",
   1.690 +        "unit_id"               INT4            REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.691 +        "area_id"               INT4,
   1.692 +        FOREIGN KEY ("unit_id", "area_id") REFERENCES "area" ("unit_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.693          "issue_id"              INT4            REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.694 +        FOREIGN KEY ("area_id", "issue_id") REFERENCES "issue" ("area_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
   1.695          "state"                 "issue_state",
   1.696          "initiative_id"         INT4,
   1.697          "draft_id"              INT8,
   1.698          "suggestion_id"         INT8,
   1.699 +        "boolean_value"         BOOLEAN,
   1.700 +        "numeric_value"         INT4,
   1.701 +        "text_value"            TEXT,
   1.702 +        "old_text_value"        TEXT,
   1.703          FOREIGN KEY ("issue_id", "initiative_id")
   1.704            REFERENCES "initiative" ("issue_id", "id")
   1.705            ON DELETE CASCADE ON UPDATE CASCADE,
   1.706          FOREIGN KEY ("initiative_id", "draft_id")
   1.707            REFERENCES "draft" ("initiative_id", "id")
   1.708            ON DELETE CASCADE ON UPDATE CASCADE,
   1.709 -        FOREIGN KEY ("initiative_id", "suggestion_id")
   1.710 -          REFERENCES "suggestion" ("initiative_id", "id")
   1.711 -          ON DELETE CASCADE ON UPDATE CASCADE,
   1.712 -        CONSTRAINT "null_constr_for_issue_state_changed" CHECK (
   1.713 +        -- NOTE: no referential integrity for suggestions because those are
   1.714 +        --       actually deleted
   1.715 +        -- FOREIGN KEY ("initiative_id", "suggestion_id")
   1.716 +        --   REFERENCES "suggestion" ("initiative_id", "id")
   1.717 +        --   ON DELETE CASCADE ON UPDATE CASCADE,
   1.718 +        CONSTRAINT "constr_for_issue_state_changed" CHECK (
   1.719            "event" != 'issue_state_changed' OR (
   1.720 -            "member_id"     ISNULL  AND
   1.721 -            "issue_id"      NOTNULL AND
   1.722 -            "state"         NOTNULL AND
   1.723 -            "initiative_id" ISNULL  AND
   1.724 -            "draft_id"      ISNULL  AND
   1.725 -            "suggestion_id" ISNULL  )),
   1.726 -        CONSTRAINT "null_constr_for_initiative_creation_or_revocation_or_new_draft" CHECK (
   1.727 +            "member_id"       ISNULL  AND
   1.728 +            "other_member_id" ISNULL  AND
   1.729 +            "scope"           ISNULL  AND
   1.730 +            "unit_id"         NOTNULL AND
   1.731 +            "area_id"         NOTNULL AND
   1.732 +            "issue_id"        NOTNULL AND
   1.733 +            "state"           NOTNULL AND
   1.734 +            "initiative_id"   ISNULL  AND
   1.735 +            "draft_id"        ISNULL  AND
   1.736 +            "suggestion_id"   ISNULL  AND
   1.737 +            "boolean_value"   ISNULL  AND
   1.738 +            "numeric_value"   ISNULL  AND
   1.739 +            "text_value"      ISNULL  AND
   1.740 +            "old_text_value"  ISNULL )),
   1.741 +        CONSTRAINT "constr_for_initiative_creation_or_revocation_or_new_draft" CHECK (
   1.742            "event" NOT IN (
   1.743              'initiative_created_in_new_issue',
   1.744              'initiative_created_in_existing_issue',
   1.745              'initiative_revoked',
   1.746              'new_draft_created'
   1.747            ) OR (
   1.748 -            "member_id"     NOTNULL AND
   1.749 -            "issue_id"      NOTNULL AND
   1.750 -            "state"         NOTNULL AND
   1.751 -            "initiative_id" NOTNULL AND
   1.752 -            "draft_id"      NOTNULL AND
   1.753 -            "suggestion_id" ISNULL  )),
   1.754 -        CONSTRAINT "null_constr_for_suggestion_creation" CHECK (
   1.755 +            "member_id"       NOTNULL AND
   1.756 +            "other_member_id" ISNULL  AND
   1.757 +            "scope"           ISNULL  AND
   1.758 +            "unit_id"         NOTNULL AND
   1.759 +            "area_id"         NOTNULL AND
   1.760 +            "issue_id"        NOTNULL AND
   1.761 +            "state"           NOTNULL AND
   1.762 +            "initiative_id"   NOTNULL AND
   1.763 +            "draft_id"        NOTNULL AND
   1.764 +            "suggestion_id"   ISNULL  AND
   1.765 +            "boolean_value"   ISNULL  AND
   1.766 +            "numeric_value"   ISNULL  AND
   1.767 +            "text_value"      ISNULL  AND
   1.768 +            "old_text_value"  ISNULL )),
   1.769 +        CONSTRAINT "constr_for_suggestion_creation" CHECK (
   1.770            "event" != 'suggestion_created' OR (
   1.771 -            "member_id"     NOTNULL AND
   1.772 -            "issue_id"      NOTNULL AND
   1.773 -            "state"         NOTNULL AND
   1.774 -            "initiative_id" NOTNULL AND
   1.775 -            "draft_id"      ISNULL  AND
   1.776 -            "suggestion_id" NOTNULL )) );
   1.777 +            "member_id"       NOTNULL AND
   1.778 +            "other_member_id" ISNULL  AND
   1.779 +            "scope"           ISNULL  AND
   1.780 +            "unit_id"         NOTNULL AND
   1.781 +            "area_id"         NOTNULL AND
   1.782 +            "issue_id"        NOTNULL AND
   1.783 +            "state"           NOTNULL AND
   1.784 +            "initiative_id"   NOTNULL AND
   1.785 +            "draft_id"        ISNULL  AND
   1.786 +            "suggestion_id"   NOTNULL AND
   1.787 +            "boolean_value"   ISNULL  AND
   1.788 +            "numeric_value"   ISNULL  AND
   1.789 +            "text_value"      ISNULL  AND
   1.790 +            "old_text_value"  ISNULL )),
   1.791 +        CONSTRAINT "constr_for_suggestion_removal" CHECK (
   1.792 +          "event" != 'suggestion_removed' OR (
   1.793 +            "member_id"       ISNULL AND
   1.794 +            "other_member_id" ISNULL  AND
   1.795 +            "scope"           ISNULL  AND
   1.796 +            "unit_id"         NOTNULL AND
   1.797 +            "area_id"         NOTNULL AND
   1.798 +            "issue_id"        NOTNULL AND
   1.799 +            "state"           NOTNULL AND
   1.800 +            "initiative_id"   NOTNULL AND
   1.801 +            "draft_id"        ISNULL  AND
   1.802 +            "suggestion_id"   NOTNULL AND
   1.803 +            "boolean_value"   ISNULL  AND
   1.804 +            "numeric_value"   ISNULL  AND
   1.805 +            "text_value"      ISNULL  AND
   1.806 +            "old_text_value"  ISNULL )),
   1.807 +        CONSTRAINT "constr_for_value_less_member_event" CHECK (
   1.808 +          "event" NOT IN (
   1.809 +            'member_activated',
   1.810 +            'member_removed',
   1.811 +            'member_profile_updated',
   1.812 +            'member_image_updated'
   1.813 +          ) OR (
   1.814 +            "member_id"       NOTNULL AND
   1.815 +            "other_member_id" ISNULL  AND
   1.816 +            "scope"           ISNULL  AND
   1.817 +            "unit_id"         ISNULL  AND
   1.818 +            "area_id"         ISNULL  AND
   1.819 +            "issue_id"        ISNULL  AND
   1.820 +            "state"           ISNULL  AND
   1.821 +            "initiative_id"   ISNULL  AND
   1.822 +            "draft_id"        ISNULL  AND
   1.823 +            "suggestion_id"   ISNULL  AND
   1.824 +            "boolean_value"   ISNULL  AND
   1.825 +            "numeric_value"   ISNULL  AND
   1.826 +            "text_value"      ISNULL  AND
   1.827 +            "old_text_value"  ISNULL )),
   1.828 +        CONSTRAINT "constr_for_member_active" CHECK (
   1.829 +          "event" != 'member_active' OR (
   1.830 +            "member_id"       NOTNULL AND
   1.831 +            "other_member_id" ISNULL  AND
   1.832 +            "scope"           ISNULL  AND
   1.833 +            "unit_id"         ISNULL  AND
   1.834 +            "area_id"         ISNULL  AND
   1.835 +            "issue_id"        ISNULL  AND
   1.836 +            "state"           ISNULL  AND
   1.837 +            "initiative_id"   ISNULL  AND
   1.838 +            "draft_id"        ISNULL  AND
   1.839 +            "suggestion_id"   ISNULL  AND
   1.840 +            "boolean_value"   NOTNULL AND
   1.841 +            "numeric_value"   ISNULL  AND
   1.842 +            "text_value"      ISNULL  AND
   1.843 +            "old_text_value"  ISNULL )),
   1.844 +        CONSTRAINT "constr_for_member_name_updated" CHECK (
   1.845 +          "event" != 'member_name_updated' OR (
   1.846 +            "member_id"       NOTNULL AND
   1.847 +            "other_member_id" ISNULL  AND
   1.848 +            "scope"           ISNULL  AND
   1.849 +            "unit_id"         ISNULL  AND
   1.850 +            "area_id"         ISNULL  AND
   1.851 +            "issue_id"        ISNULL  AND
   1.852 +            "state"           ISNULL  AND
   1.853 +            "initiative_id"   ISNULL  AND
   1.854 +            "draft_id"        ISNULL  AND
   1.855 +            "suggestion_id"   ISNULL  AND
   1.856 +            "boolean_value"   ISNULL  AND
   1.857 +            "numeric_value"   ISNULL  AND
   1.858 +            "text_value"      NOTNULL AND
   1.859 +            "old_text_value"  NOTNULL )),
   1.860 +        CONSTRAINT "constr_for_interest" CHECK (
   1.861 +          "event" != 'interest' OR (
   1.862 +            "member_id"       NOTNULL AND
   1.863 +            "other_member_id" ISNULL  AND
   1.864 +            "scope"           ISNULL  AND
   1.865 +            "unit_id"         NOTNULL AND
   1.866 +            "area_id"         NOTNULL AND
   1.867 +            "issue_id"        NOTNULL AND
   1.868 +            "state"           NOTNULL AND
   1.869 +            "initiative_id"   ISNULL  AND
   1.870 +            "draft_id"        ISNULL  AND
   1.871 +            "suggestion_id"   ISNULL  AND
   1.872 +            "boolean_value"   NOTNULL AND
   1.873 +            "numeric_value"   ISNULL  AND
   1.874 +            "text_value"      ISNULL  AND
   1.875 +            "old_text_value"  ISNULL )),
   1.876 +        CONSTRAINT "constr_for_initiator" CHECK (
   1.877 +          "event" != 'initiator' OR (
   1.878 +            "member_id"       NOTNULL AND
   1.879 +            "other_member_id" ISNULL  AND
   1.880 +            "scope"           ISNULL  AND
   1.881 +            "unit_id"         NOTNULL AND
   1.882 +            "area_id"         NOTNULL AND
   1.883 +            "issue_id"        NOTNULL AND
   1.884 +            "state"           NOTNULL AND
   1.885 +            "initiative_id"   NOTNULL AND
   1.886 +            "draft_id"        ISNULL  AND
   1.887 +            "suggestion_id"   ISNULL  AND
   1.888 +            "boolean_value"   NOTNULL AND
   1.889 +            "numeric_value"   ISNULL  AND
   1.890 +            "text_value"      ISNULL  AND
   1.891 +            "old_text_value"  ISNULL )),
   1.892 +        CONSTRAINT "constr_for_support" CHECK (
   1.893 +          "event" != 'support' OR (
   1.894 +            "member_id"       NOTNULL AND
   1.895 +            "other_member_id" ISNULL  AND
   1.896 +            "scope"           ISNULL  AND
   1.897 +            "unit_id"         NOTNULL AND
   1.898 +            "area_id"         NOTNULL AND
   1.899 +            "issue_id"        NOTNULL AND
   1.900 +            "state"           NOTNULL AND
   1.901 +            "initiative_id"   NOTNULL AND
   1.902 +            ("draft_id" NOTNULL) = ("boolean_value" = TRUE) AND
   1.903 +            "suggestion_id"   ISNULL  AND
   1.904 +            "boolean_value"   NOTNULL AND
   1.905 +            "numeric_value"   ISNULL  AND
   1.906 +            "text_value"      ISNULL  AND
   1.907 +            "old_text_value"  ISNULL )),
   1.908 +        CONSTRAINT "constr_for_support_updated" CHECK (
   1.909 +          "event" != 'support_updated' OR (
   1.910 +            "member_id"       NOTNULL AND
   1.911 +            "other_member_id" ISNULL  AND
   1.912 +            "scope"           ISNULL  AND
   1.913 +            "unit_id"         NOTNULL AND
   1.914 +            "area_id"         NOTNULL AND
   1.915 +            "issue_id"        NOTNULL AND
   1.916 +            "state"           NOTNULL AND
   1.917 +            "initiative_id"   NOTNULL AND
   1.918 +            "draft_id"        NOTNULL AND
   1.919 +            "suggestion_id"   ISNULL  AND
   1.920 +            "boolean_value"   ISNULL  AND
   1.921 +            "numeric_value"   ISNULL  AND
   1.922 +            "text_value"      ISNULL  AND
   1.923 +            "old_text_value"  ISNULL )),
   1.924 +        CONSTRAINT "constr_for_suggestion_rated" CHECK (
   1.925 +          "event" != 'suggestion_rated' OR (
   1.926 +            "member_id"       NOTNULL AND
   1.927 +            "other_member_id" ISNULL  AND
   1.928 +            "scope"           ISNULL  AND
   1.929 +            "unit_id"         NOTNULL AND
   1.930 +            "area_id"         NOTNULL AND
   1.931 +            "issue_id"        NOTNULL AND
   1.932 +            "state"           NOTNULL AND
   1.933 +            "initiative_id"   NOTNULL AND
   1.934 +            "draft_id"        ISNULL  AND
   1.935 +            "suggestion_id"   NOTNULL AND
   1.936 +            ("boolean_value" NOTNULL) = ("numeric_value" != 0) AND
   1.937 +            "numeric_value"   NOTNULL AND
   1.938 +            "numeric_value" IN (-2, -1, 0, 1, 2) AND
   1.939 +            "text_value"      ISNULL  AND
   1.940 +            "old_text_value"  ISNULL )),
   1.941 +        CONSTRAINT "constr_for_delegation" CHECK (
   1.942 +          "event" != 'delegation' OR (
   1.943 +            "member_id"       NOTNULL AND
   1.944 +            ("other_member_id" NOTNULL) OR ("boolean_value" = FALSE) AND
   1.945 +            "scope"           NOTNULL AND
   1.946 +            "unit_id"         NOTNULL AND
   1.947 +            ("area_id"  NOTNULL) = ("scope" != 'unit'::"delegation_scope") AND
   1.948 +            ("issue_id" NOTNULL) = ("scope" = 'issue'::"delegation_scope") AND
   1.949 +            ("state"    NOTNULL) = ("scope" = 'issue'::"delegation_scope") AND
   1.950 +            "initiative_id"   ISNULL  AND
   1.951 +            "draft_id"        ISNULL  AND
   1.952 +            "suggestion_id"   ISNULL  AND
   1.953 +            "boolean_value"   NOTNULL AND
   1.954 +            "numeric_value"   ISNULL  AND
   1.955 +            "text_value"      ISNULL  AND
   1.956 +            "old_text_value"  ISNULL )),
   1.957 +        CONSTRAINT "constr_for_contact" CHECK (
   1.958 +          "event" != 'contact' OR (
   1.959 +            "member_id"       NOTNULL AND
   1.960 +            "other_member_id" NOTNULL AND
   1.961 +            "scope"           ISNULL  AND
   1.962 +            "unit_id"         ISNULL  AND
   1.963 +            "area_id"         ISNULL  AND
   1.964 +            "issue_id"        ISNULL  AND
   1.965 +            "state"           ISNULL  AND
   1.966 +            "initiative_id"   ISNULL  AND
   1.967 +            "draft_id"        ISNULL  AND
   1.968 +            "suggestion_id"   ISNULL  AND
   1.969 +            "boolean_value"   NOTNULL AND
   1.970 +            "numeric_value"   ISNULL  AND
   1.971 +            "text_value"      ISNULL  AND
   1.972 +            "old_text_value"  ISNULL )) );
   1.973  CREATE INDEX "event_occurrence_idx" ON "event" ("occurrence");
   1.974  
   1.975  COMMENT ON TABLE "event" IS 'Event table, automatically filled by triggers';
   1.976 @@ -1399,10 +1760,19 @@
   1.977  CREATE FUNCTION "write_event_issue_state_changed_trigger"()
   1.978    RETURNS TRIGGER
   1.979    LANGUAGE 'plpgsql' VOLATILE AS $$
   1.980 +    DECLARE
   1.981 +      "area_row" "area"%ROWTYPE;
   1.982      BEGIN
   1.983        IF NEW."state" != OLD."state" THEN
   1.984 -        INSERT INTO "event" ("event", "issue_id", "state")
   1.985 -          VALUES ('issue_state_changed', NEW."id", NEW."state");
   1.986 +        SELECT * INTO "area_row" FROM "area" WHERE "id" = NEW."area_id"
   1.987 +          FOR SHARE;
   1.988 +        INSERT INTO "event" (
   1.989 +            "event",
   1.990 +            "unit_id", "area_id", "issue_id", "state"
   1.991 +          ) VALUES (
   1.992 +            'issue_state_changed',
   1.993 +            "area_row"."unit_id", NEW."area_id", NEW."id", NEW."state"
   1.994 +          );
   1.995        END IF;
   1.996        RETURN NULL;
   1.997      END;
   1.998 @@ -1422,16 +1792,19 @@
   1.999      DECLARE
  1.1000        "initiative_row" "initiative"%ROWTYPE;
  1.1001        "issue_row"      "issue"%ROWTYPE;
  1.1002 +      "area_row"       "area"%ROWTYPE;
  1.1003        "event_v"        "event_type";
  1.1004      BEGIN
  1.1005        SELECT * INTO "initiative_row" FROM "initiative"
  1.1006 -        WHERE "id" = NEW."initiative_id";
  1.1007 +        WHERE "id" = NEW."initiative_id" FOR SHARE;
  1.1008        SELECT * INTO "issue_row" FROM "issue"
  1.1009 -        WHERE "id" = "initiative_row"."issue_id";
  1.1010 +        WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  1.1011 +      SELECT * INTO "area_row" FROM "area"
  1.1012 +        WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1013        IF EXISTS (
  1.1014          SELECT NULL FROM "draft"
  1.1015 -        WHERE "initiative_id" = NEW."initiative_id"
  1.1016 -        AND "id" != NEW."id"
  1.1017 +        WHERE "initiative_id" = NEW."initiative_id" AND "id" != NEW."id"
  1.1018 +        FOR SHARE
  1.1019        ) THEN
  1.1020          "event_v" := 'new_draft_created';
  1.1021        ELSE
  1.1022 @@ -1439,6 +1812,7 @@
  1.1023            SELECT NULL FROM "initiative"
  1.1024            WHERE "issue_id" = "initiative_row"."issue_id"
  1.1025            AND "id" != "initiative_row"."id"
  1.1026 +          FOR SHARE
  1.1027          ) THEN
  1.1028            "event_v" := 'initiative_created_in_existing_issue';
  1.1029          ELSE
  1.1030 @@ -1447,14 +1821,14 @@
  1.1031        END IF;
  1.1032        INSERT INTO "event" (
  1.1033            "event", "member_id",
  1.1034 -          "issue_id", "state", "initiative_id", "draft_id"
  1.1035 +          "unit_id", "area_id", "issue_id", "state",
  1.1036 +          "initiative_id", "draft_id"
  1.1037          ) VALUES (
  1.1038 -          "event_v",
  1.1039 -          NEW."author_id",
  1.1040 -          "initiative_row"."issue_id",
  1.1041 -          "issue_row"."state",
  1.1042 -          "initiative_row"."id",
  1.1043 -          NEW."id" );
  1.1044 +          "event_v", NEW."author_id",
  1.1045 +          "area_row"."unit_id", "issue_row"."area_id",
  1.1046 +          "initiative_row"."issue_id", "issue_row"."state",
  1.1047 +          NEW."initiative_id", NEW."id"
  1.1048 +        );
  1.1049        RETURN NULL;
  1.1050      END;
  1.1051    $$;
  1.1052 @@ -1472,22 +1846,26 @@
  1.1053    LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1054      DECLARE
  1.1055        "issue_row"  "issue"%ROWTYPE;
  1.1056 +      "area_row"   "area"%ROWTYPE;
  1.1057        "draft_id_v" "draft"."id"%TYPE;
  1.1058      BEGIN
  1.1059        IF OLD."revoked" ISNULL AND NEW."revoked" NOTNULL THEN
  1.1060          SELECT * INTO "issue_row" FROM "issue"
  1.1061 -          WHERE "id" = NEW."issue_id";
  1.1062 +          WHERE "id" = NEW."issue_id" FOR SHARE;
  1.1063 +        SELECT * INTO "area_row" FROM "area"
  1.1064 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1065          SELECT "id" INTO "draft_id_v" FROM "current_draft"
  1.1066 -          WHERE "initiative_id" = NEW."id";
  1.1067 +          WHERE "initiative_id" = NEW."id" FOR SHARE;
  1.1068          INSERT INTO "event" (
  1.1069 -            "event", "member_id", "issue_id", "state", "initiative_id", "draft_id"
  1.1070 +            "event", "member_id",
  1.1071 +            "unit_id", "area_id", "issue_id", "state",
  1.1072 +            "initiative_id", "draft_id"
  1.1073            ) VALUES (
  1.1074 -            'initiative_revoked',
  1.1075 -            NEW."revoked_by_member_id",
  1.1076 -            NEW."issue_id",
  1.1077 -            "issue_row"."state",
  1.1078 -            NEW."id",
  1.1079 -            "draft_id_v");
  1.1080 +            'initiative_revoked', NEW."revoked_by_member_id",
  1.1081 +            "area_row"."unit_id", "issue_row"."area_id",
  1.1082 +            NEW."issue_id", "issue_row"."state",
  1.1083 +            NEW."id", "draft_id_v"
  1.1084 +          );
  1.1085        END IF;
  1.1086        RETURN NULL;
  1.1087      END;
  1.1088 @@ -1507,21 +1885,24 @@
  1.1089      DECLARE
  1.1090        "initiative_row" "initiative"%ROWTYPE;
  1.1091        "issue_row"      "issue"%ROWTYPE;
  1.1092 +      "area_row"       "area"%ROWTYPE;
  1.1093      BEGIN
  1.1094        SELECT * INTO "initiative_row" FROM "initiative"
  1.1095 -        WHERE "id" = NEW."initiative_id";
  1.1096 +        WHERE "id" = NEW."initiative_id" FOR SHARE;
  1.1097        SELECT * INTO "issue_row" FROM "issue"
  1.1098 -        WHERE "id" = "initiative_row"."issue_id";
  1.1099 +        WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  1.1100 +      SELECT * INTO "area_row" FROM "area"
  1.1101 +        WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1102        INSERT INTO "event" (
  1.1103            "event", "member_id",
  1.1104 -          "issue_id", "state", "initiative_id", "suggestion_id"
  1.1105 +          "unit_id", "area_id", "issue_id", "state",
  1.1106 +          "initiative_id", "suggestion_id"
  1.1107          ) VALUES (
  1.1108 -          'suggestion_created',
  1.1109 -          NEW."author_id",
  1.1110 -          "initiative_row"."issue_id",
  1.1111 -          "issue_row"."state",
  1.1112 -          "initiative_row"."id",
  1.1113 -          NEW."id" );
  1.1114 +          'suggestion_created', NEW."author_id",
  1.1115 +          "area_row"."unit_id", "issue_row"."area_id",
  1.1116 +          "initiative_row"."issue_id", "issue_row"."state",
  1.1117 +          NEW."initiative_id", NEW."id"
  1.1118 +        );
  1.1119        RETURN NULL;
  1.1120      END;
  1.1121    $$;
  1.1122 @@ -1534,12 +1915,675 @@
  1.1123  COMMENT ON TRIGGER "write_event_suggestion_created" ON "suggestion" IS 'Create entry in "event" table on suggestion creation';
  1.1124  
  1.1125  
  1.1126 +CREATE FUNCTION "write_event_suggestion_removed_trigger"()
  1.1127 +  RETURNS TRIGGER
  1.1128 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1129 +    DECLARE
  1.1130 +      "initiative_row" "initiative"%ROWTYPE;
  1.1131 +      "issue_row"      "issue"%ROWTYPE;
  1.1132 +      "area_row"       "area"%ROWTYPE;
  1.1133 +    BEGIN
  1.1134 +      SELECT * INTO "initiative_row" FROM "initiative"
  1.1135 +        WHERE "id" = OLD."initiative_id" FOR SHARE;
  1.1136 +      IF "initiative_row"."id" NOTNULL THEN
  1.1137 +        SELECT * INTO "issue_row" FROM "issue"
  1.1138 +          WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  1.1139 +        SELECT * INTO "area_row" FROM "area"
  1.1140 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1141 +        INSERT INTO "event" (
  1.1142 +            "event",
  1.1143 +            "unit_id", "area_id", "issue_id", "state",
  1.1144 +            "initiative_id", "suggestion_id"
  1.1145 +          ) VALUES (
  1.1146 +            'suggestion_removed',
  1.1147 +            "area_row"."unit_id", "issue_row"."area_id",
  1.1148 +            "initiative_row"."issue_id", "issue_row"."state",
  1.1149 +            OLD."initiative_id", OLD."id"
  1.1150 +          );
  1.1151 +      END IF;
  1.1152 +      RETURN NULL;
  1.1153 +    END;
  1.1154 +  $$;
  1.1155 +
  1.1156 +CREATE TRIGGER "write_event_suggestion_removed"
  1.1157 +  AFTER DELETE ON "suggestion" FOR EACH ROW EXECUTE PROCEDURE
  1.1158 +  "write_event_suggestion_removed_trigger"();
  1.1159 +
  1.1160 +COMMENT ON FUNCTION "write_event_suggestion_removed_trigger"()      IS 'Implementation of trigger "write_event_suggestion_removed" on table "issue"';
  1.1161 +COMMENT ON TRIGGER "write_event_suggestion_removed" ON "suggestion" IS 'Create entry in "event" table on suggestion creation';
  1.1162 +
  1.1163 +
  1.1164 +CREATE FUNCTION "write_event_member_trigger"()
  1.1165 +  RETURNS TRIGGER
  1.1166 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1167 +    BEGIN
  1.1168 +      IF TG_OP = 'INSERT' THEN
  1.1169 +        IF NEW."activated" NOTNULL THEN
  1.1170 +          INSERT INTO "event" ("event", "member_id")
  1.1171 +            VALUES ('member_activated', NEW."id");
  1.1172 +        END IF;
  1.1173 +        IF NEW."active" THEN
  1.1174 +          INSERT INTO "event" ("event", "member_id", "boolean_value")
  1.1175 +            VALUES ('member_active', NEW."id", TRUE);
  1.1176 +        END IF;
  1.1177 +      ELSIF TG_OP = 'UPDATE' THEN
  1.1178 +        IF OLD."id" != NEW."id" THEN
  1.1179 +          RAISE EXCEPTION 'Cannot change member ID';
  1.1180 +        END IF;
  1.1181 +        IF OLD."name" != NEW."name" THEN
  1.1182 +          INSERT INTO "event" (
  1.1183 +            "event", "member_id", "text_value", "old_text_value"
  1.1184 +          ) VALUES (
  1.1185 +            'member_name_updated', NEW."id", NEW."name", OLD."name"
  1.1186 +          );
  1.1187 +        END IF;
  1.1188 +        IF OLD."active" != NEW."active" THEN
  1.1189 +          INSERT INTO "event" ("event", "member_id", "boolean_value") VALUES (
  1.1190 +            'member_active', NEW."id", NEW."active"
  1.1191 +          );
  1.1192 +        END IF;
  1.1193 +        IF
  1.1194 +          OLD."activated" NOTNULL AND
  1.1195 +          NEW."last_login"      ISNULL AND
  1.1196 +          NEW."login"           ISNULL AND
  1.1197 +          NEW."authority_login" ISNULL AND
  1.1198 +          NEW."locked"          = TRUE
  1.1199 +        THEN
  1.1200 +          INSERT INTO "event" ("event", "member_id")
  1.1201 +            VALUES ('member_removed', NEW."id");
  1.1202 +        END IF;
  1.1203 +      END IF;
  1.1204 +      RETURN NULL;
  1.1205 +    END;
  1.1206 +  $$;
  1.1207 +
  1.1208 +CREATE TRIGGER "write_event_member"
  1.1209 +  AFTER INSERT OR UPDATE ON "member" FOR EACH ROW EXECUTE PROCEDURE
  1.1210 +  "write_event_member_trigger"();
  1.1211 +
  1.1212 +COMMENT ON FUNCTION "write_event_member_trigger"()  IS 'Implementation of trigger "write_event_member" on table "member"';
  1.1213 +COMMENT ON TRIGGER "write_event_member" ON "member" IS 'Create entries in "event" table on insertion to member table';
  1.1214 +
  1.1215 +
  1.1216 +CREATE FUNCTION "write_event_member_profile_updated_trigger"()
  1.1217 +  RETURNS TRIGGER
  1.1218 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1219 +    BEGIN
  1.1220 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  1.1221 +        IF EXISTS (SELECT NULL FROM "member" WHERE "id" = OLD."member_id") THEN
  1.1222 +          INSERT INTO "event" ("event", "member_id") VALUES (
  1.1223 +            'member_profile_updated', OLD."member_id"
  1.1224 +          );
  1.1225 +        END IF;
  1.1226 +      END IF;
  1.1227 +      IF TG_OP = 'UPDATE' THEN
  1.1228 +        IF OLD."member_id" = NEW."member_id" THEN
  1.1229 +          RETURN NULL;
  1.1230 +        END IF;
  1.1231 +      END IF;
  1.1232 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  1.1233 +        INSERT INTO "event" ("event", "member_id") VALUES (
  1.1234 +          'member_profile_updated', NEW."member_id"
  1.1235 +        );
  1.1236 +      END IF;
  1.1237 +      RETURN NULL;
  1.1238 +    END;
  1.1239 +  $$;
  1.1240 +
  1.1241 +CREATE TRIGGER "write_event_member_profile_updated"
  1.1242 +  AFTER INSERT OR UPDATE OR DELETE ON "member_profile"
  1.1243 +  FOR EACH ROW EXECUTE PROCEDURE
  1.1244 +  "write_event_member_profile_updated_trigger"();
  1.1245 +
  1.1246 +COMMENT ON FUNCTION "write_event_member_profile_updated_trigger"()          IS 'Implementation of trigger "write_event_member_profile_updated" on table "member_profile"';
  1.1247 +COMMENT ON TRIGGER "write_event_member_profile_updated" ON "member_profile" IS 'Creates entries in "event" table on member profile update';
  1.1248 +
  1.1249 +
  1.1250 +CREATE FUNCTION "write_event_member_image_updated_trigger"()
  1.1251 +  RETURNS TRIGGER
  1.1252 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1253 +    BEGIN
  1.1254 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  1.1255 +        IF NOT OLD."scaled" THEN
  1.1256 +          IF EXISTS (SELECT NULL FROM "member" WHERE "id" = OLD."member_id") THEN
  1.1257 +            INSERT INTO "event" ("event", "member_id") VALUES (
  1.1258 +              'member_image_updated', OLD."member_id"
  1.1259 +            );
  1.1260 +          END IF;
  1.1261 +        END IF;
  1.1262 +      END IF;
  1.1263 +      IF TG_OP = 'UPDATE' THEN
  1.1264 +        IF
  1.1265 +          OLD."member_id" = NEW."member_id" AND
  1.1266 +          OLD."scaled" = NEW."scaled"
  1.1267 +        THEN
  1.1268 +          RETURN NULL;
  1.1269 +        END IF;
  1.1270 +      END IF;
  1.1271 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  1.1272 +        IF NOT NEW."scaled" THEN
  1.1273 +          INSERT INTO "event" ("event", "member_id") VALUES (
  1.1274 +            'member_image_updated', NEW."member_id"
  1.1275 +          );
  1.1276 +        END IF;
  1.1277 +      END IF;
  1.1278 +      RETURN NULL;
  1.1279 +    END;
  1.1280 +  $$;
  1.1281 +
  1.1282 +CREATE TRIGGER "write_event_member_image_updated"
  1.1283 +  AFTER INSERT OR UPDATE OR DELETE ON "member_image"
  1.1284 +  FOR EACH ROW EXECUTE PROCEDURE
  1.1285 +  "write_event_member_image_updated_trigger"();
  1.1286 +
  1.1287 +COMMENT ON FUNCTION "write_event_member_image_updated_trigger"()        IS 'Implementation of trigger "write_event_member_image_updated" on table "member_image"';
  1.1288 +COMMENT ON TRIGGER "write_event_member_image_updated" ON "member_image" IS 'Creates entries in "event" table on member image update';
  1.1289 +
  1.1290 +
  1.1291 +CREATE FUNCTION "write_event_interest_trigger"()
  1.1292 +  RETURNS TRIGGER
  1.1293 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1294 +    DECLARE
  1.1295 +      "issue_row" "issue"%ROWTYPE;
  1.1296 +      "area_row"  "area"%ROWTYPE;
  1.1297 +    BEGIN
  1.1298 +      IF TG_OP = 'UPDATE' THEN
  1.1299 +        IF OLD = NEW THEN
  1.1300 +          RETURN NULL;
  1.1301 +        END IF;
  1.1302 +      END IF;
  1.1303 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  1.1304 +        SELECT * INTO "issue_row" FROM "issue"
  1.1305 +          WHERE "id" = OLD."issue_id" FOR SHARE;
  1.1306 +        SELECT * INTO "area_row" FROM "area"
  1.1307 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1308 +        IF "issue_row"."id" NOTNULL THEN
  1.1309 +          INSERT INTO "event" (
  1.1310 +              "event", "member_id",
  1.1311 +              "unit_id", "area_id", "issue_id", "state",
  1.1312 +              "boolean_value"
  1.1313 +            ) VALUES (
  1.1314 +              'interest', OLD."member_id",
  1.1315 +              "area_row"."unit_id", "issue_row"."area_id",
  1.1316 +              OLD."issue_id", "issue_row"."state",
  1.1317 +              FALSE
  1.1318 +            );
  1.1319 +        END IF;
  1.1320 +      END IF;
  1.1321 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  1.1322 +        SELECT * INTO "issue_row" FROM "issue"
  1.1323 +          WHERE "id" = NEW."issue_id" FOR SHARE;
  1.1324 +        SELECT * INTO "area_row" FROM "area"
  1.1325 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1326 +        INSERT INTO "event" (
  1.1327 +            "event", "member_id",
  1.1328 +            "unit_id", "area_id", "issue_id", "state",
  1.1329 +            "boolean_value"
  1.1330 +          ) VALUES (
  1.1331 +            'interest', NEW."member_id",
  1.1332 +            "area_row"."unit_id", "issue_row"."area_id",
  1.1333 +            NEW."issue_id", "issue_row"."state",
  1.1334 +            TRUE
  1.1335 +          );
  1.1336 +      END IF;
  1.1337 +      RETURN NULL;
  1.1338 +    END;
  1.1339 +  $$;
  1.1340 +
  1.1341 +CREATE TRIGGER "write_event_interest"
  1.1342 +  AFTER INSERT OR UPDATE OR DELETE ON "interest" FOR EACH ROW EXECUTE PROCEDURE
  1.1343 +  "write_event_interest_trigger"();
  1.1344 +
  1.1345 +COMMENT ON FUNCTION "write_event_interest_trigger"()  IS 'Implementation of trigger "write_event_interest_inserted" on table "interest"';
  1.1346 +COMMENT ON TRIGGER "write_event_interest" ON "interest" IS 'Create entry in "event" table on adding or removing interest';
  1.1347 +
  1.1348 +
  1.1349 +CREATE FUNCTION "write_event_initiator_trigger"()
  1.1350 +  RETURNS TRIGGER
  1.1351 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1352 +    DECLARE
  1.1353 +      "initiative_row" "initiative"%ROWTYPE;
  1.1354 +      "issue_row"      "issue"%ROWTYPE;
  1.1355 +      "area_row"       "area"%ROWTYPE;
  1.1356 +    BEGIN
  1.1357 +      IF TG_OP = 'UPDATE' THEN
  1.1358 +        IF
  1.1359 +          OLD."initiative_id" = NEW."initiative_id" AND
  1.1360 +          OLD."member_id" = NEW."member_id" AND
  1.1361 +          coalesce(OLD."accepted", FALSE) = coalesce(NEW."accepted", FALSE)
  1.1362 +        THEN
  1.1363 +          RETURN NULL;
  1.1364 +        END IF;
  1.1365 +      END IF;
  1.1366 +      IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND NOT "accepted_v" THEN
  1.1367 +        IF coalesce(OLD."accepted", FALSE) = TRUE THEN
  1.1368 +          SELECT * INTO "initiative_row" FROM "initiative"
  1.1369 +            WHERE "id" = OLD."initiative_id" FOR SHARE;
  1.1370 +          IF "initiative_row"."id" NOTNULL THEN
  1.1371 +            SELECT * INTO "issue_row" FROM "issue"
  1.1372 +              WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  1.1373 +            SELECT * INTO "area_row" FROM "area"
  1.1374 +              WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1375 +            INSERT INTO "event" (
  1.1376 +                "event", "member_id",
  1.1377 +                "unit_id", "area_id", "issue_id", "state",
  1.1378 +                "initiative_id", "boolean_value"
  1.1379 +              ) VALUES (
  1.1380 +                'initiator', OLD."member_id",
  1.1381 +                "area_row"."unit_id", "issue_row"."area_id",
  1.1382 +                "issue_row"."id", "issue_row"."state",
  1.1383 +                OLD."initiative_id", FALSE
  1.1384 +              );
  1.1385 +          END IF;
  1.1386 +        END IF;
  1.1387 +      END IF;
  1.1388 +      IF TG_OP = 'UPDATE' AND NOT "rejected_v" THEN
  1.1389 +        IF coalesce(NEW."accepted", FALSE) = TRUE THEN
  1.1390 +          SELECT * INTO "initiative_row" FROM "initiative"
  1.1391 +            WHERE "id" = NEW."initiative_id" FOR SHARE;
  1.1392 +          SELECT * INTO "issue_row" FROM "issue"
  1.1393 +            WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  1.1394 +          SELECT * INTO "area_row" FROM "area"
  1.1395 +            WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1396 +          INSERT INTO "event" (
  1.1397 +              "event", "member_id",
  1.1398 +              "unit_id", "area_id", "issue_id", "state",
  1.1399 +              "initiative_id", "boolean_value"
  1.1400 +            ) VALUES (
  1.1401 +              'initiator', NEW."member_id",
  1.1402 +              "area_row"."unit_id", "issue_row"."area_id",
  1.1403 +              "issue_row"."id", "issue_row"."state",
  1.1404 +              NEW."initiative_id", TRUE
  1.1405 +            );
  1.1406 +        END IF;
  1.1407 +      END IF;
  1.1408 +      RETURN NULL;
  1.1409 +    END;
  1.1410 +  $$;
  1.1411 +
  1.1412 +CREATE TRIGGER "write_event_initiator"
  1.1413 +  AFTER UPDATE OR DELETE ON "initiator" FOR EACH ROW EXECUTE PROCEDURE
  1.1414 +  "write_event_initiator_trigger"();
  1.1415 +
  1.1416 +COMMENT ON FUNCTION "write_event_initiator_trigger"()     IS 'Implementation of trigger "write_event_initiator" on table "initiator"';
  1.1417 +COMMENT ON TRIGGER "write_event_initiator" ON "initiator" IS 'Create entry in "event" table when accepting or removing initiatorship (NOTE: trigger does not fire on INSERT to avoid events on initiative creation)';
  1.1418 +
  1.1419 +
  1.1420 +CREATE FUNCTION "write_event_support_trigger"()
  1.1421 +  RETURNS TRIGGER
  1.1422 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1423 +    DECLARE
  1.1424 +      "issue_row" "issue"%ROWTYPE;
  1.1425 +      "area_row"  "area"%ROWTYPE;
  1.1426 +    BEGIN
  1.1427 +      IF TG_OP = 'UPDATE' THEN
  1.1428 +        IF
  1.1429 +          OLD."initiative_id" = NEW."initiative_id" AND
  1.1430 +          OLD."member_id" = NEW."member_id"
  1.1431 +        THEN
  1.1432 +          IF OLD."draft_id" != NEW."draft_id" THEN
  1.1433 +            SELECT * INTO "issue_row" FROM "issue"
  1.1434 +              WHERE "id" = NEW."issue_id" FOR SHARE;
  1.1435 +            SELECT * INTO "area_row" FROM "area"
  1.1436 +              WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1437 +            INSERT INTO "event" (
  1.1438 +                "event", "member_id",
  1.1439 +                "unit_id", "area_id", "issue_id", "state",
  1.1440 +                "initiative_id", "draft_id"
  1.1441 +              ) VALUES (
  1.1442 +                'support_updated', NEW."member_id",
  1.1443 +                "area_row"."unit_id", "issue_row"."area_id",
  1.1444 +                "issue_row"."id", "issue_row"."state",
  1.1445 +                NEW."initiative_id", NEW."draft_id"
  1.1446 +              );
  1.1447 +          END IF;
  1.1448 +          RETURN NULL;
  1.1449 +        END IF;
  1.1450 +      END IF;
  1.1451 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  1.1452 +        IF EXISTS (
  1.1453 +          SELECT NULL FROM "initiative" WHERE "id" = OLD."initiative_id"
  1.1454 +          FOR SHARE
  1.1455 +        ) THEN
  1.1456 +          SELECT * INTO "issue_row" FROM "issue"
  1.1457 +            WHERE "id" = OLD."issue_id" FOR SHARE;
  1.1458 +          SELECT * INTO "area_row" FROM "area"
  1.1459 +            WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1460 +          INSERT INTO "event" (
  1.1461 +              "event", "member_id",
  1.1462 +              "unit_id", "area_id", "issue_id", "state",
  1.1463 +              "initiative_id", "draft_id", "boolean_value"
  1.1464 +            ) VALUES (
  1.1465 +              'support', OLD."member_id",
  1.1466 +              "area_row"."unit_id", "issue_row"."area_id",
  1.1467 +              "issue_row"."id", "issue_row"."state",
  1.1468 +              OLD."initiative_id", OLD."draft_id", FALSE
  1.1469 +            );
  1.1470 +        END IF;
  1.1471 +      END IF;
  1.1472 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  1.1473 +        SELECT * INTO "issue_row" FROM "issue"
  1.1474 +          WHERE "id" = NEW."issue_id" FOR SHARE;
  1.1475 +        SELECT * INTO "area_row" FROM "area"
  1.1476 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1477 +        INSERT INTO "event" (
  1.1478 +            "event", "member_id",
  1.1479 +            "unit_id", "area_id", "issue_id", "state",
  1.1480 +            "initiative_id", "draft_id", "boolean_value"
  1.1481 +          ) VALUES (
  1.1482 +            'support', NEW."member_id",
  1.1483 +            "area_row"."unit_id", "issue_row"."area_id",
  1.1484 +            "issue_row"."id", "issue_row"."state",
  1.1485 +            NEW."initiative_id", NEW."draft_id", TRUE
  1.1486 +          );
  1.1487 +      END IF;
  1.1488 +      RETURN NULL;
  1.1489 +    END;
  1.1490 +  $$;
  1.1491 +
  1.1492 +CREATE TRIGGER "write_event_support"
  1.1493 +  AFTER INSERT OR UPDATE OR DELETE ON "supporter" FOR EACH ROW EXECUTE PROCEDURE
  1.1494 +  "write_event_support_trigger"();
  1.1495 +
  1.1496 +COMMENT ON FUNCTION "write_event_support_trigger"()     IS 'Implementation of trigger "write_event_support" on table "supporter"';
  1.1497 +COMMENT ON TRIGGER "write_event_support" ON "supporter" IS 'Create entry in "event" table when adding, updating, or removing support';
  1.1498 +
  1.1499 +
  1.1500 +CREATE FUNCTION "write_event_suggestion_rated_trigger"()
  1.1501 +  RETURNS TRIGGER
  1.1502 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1503 +    DECLARE
  1.1504 +      "same_pkey_v"    BOOLEAN = FALSE;
  1.1505 +      "initiative_row" "initiative"%ROWTYPE;
  1.1506 +      "issue_row"      "issue"%ROWTYPE;
  1.1507 +      "area_row"       "area"%ROWTYPE;
  1.1508 +    BEGIN
  1.1509 +      IF TG_OP = 'UPDATE' THEN
  1.1510 +        IF
  1.1511 +          OLD."suggestion_id" = NEW."suggestion_id" AND
  1.1512 +          OLD."member_id"     = NEW."member_id"
  1.1513 +        THEN
  1.1514 +          IF
  1.1515 +            OLD."degree"    = NEW."degree" AND
  1.1516 +            OLD."fulfilled" = NEW."fulfilled"
  1.1517 +          THEN
  1.1518 +            RETURN NULL;
  1.1519 +          END IF;
  1.1520 +          "same_pkey_v" := TRUE;
  1.1521 +        END IF;
  1.1522 +      END IF;
  1.1523 +      IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND NOT "same_pkey_v" THEN
  1.1524 +        IF EXISTS (
  1.1525 +          SELECT NULL FROM "suggestion" WHERE "id" = OLD."suggestion_id"
  1.1526 +          FOR SHARE
  1.1527 +        ) THEN
  1.1528 +          SELECT * INTO "initiative_row" FROM "initiative"
  1.1529 +            WHERE "id" = OLD."initiative_id" FOR SHARE;
  1.1530 +          SELECT * INTO "issue_row" FROM "issue"
  1.1531 +            WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  1.1532 +          SELECT * INTO "area_row" FROM "area"
  1.1533 +            WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1534 +          INSERT INTO "event" (
  1.1535 +              "event", "member_id",
  1.1536 +              "unit_id", "area_id", "issue_id", "state",
  1.1537 +              "initiative_id", "suggestion_id",
  1.1538 +              "boolean_value", "numeric_value"
  1.1539 +            ) VALUES (
  1.1540 +              'suggestion_rated', OLD."member_id",
  1.1541 +              "area_row"."unit_id", "issue_row"."area_id",
  1.1542 +              "initiative_row"."issue_id", "issue_row"."state",
  1.1543 +              OLD."initiative_id", OLD."suggestion_id",
  1.1544 +              NULL, 0
  1.1545 +            );
  1.1546 +        END IF;
  1.1547 +      END IF;
  1.1548 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  1.1549 +        SELECT * INTO "initiative_row" FROM "initiative"
  1.1550 +          WHERE "id" = NEW."initiative_id" FOR SHARE;
  1.1551 +        SELECT * INTO "issue_row" FROM "issue"
  1.1552 +          WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  1.1553 +        SELECT * INTO "area_row" FROM "area"
  1.1554 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  1.1555 +        INSERT INTO "event" (
  1.1556 +            "event", "member_id",
  1.1557 +            "unit_id", "area_id", "issue_id", "state",
  1.1558 +            "initiative_id", "suggestion_id",
  1.1559 +            "boolean_value", "numeric_value"
  1.1560 +          ) VALUES (
  1.1561 +            'suggestion_rated', NEW."member_id",
  1.1562 +            "area_row"."unit_id", "issue_row"."area_id",
  1.1563 +            "initiative_row"."issue_id", "issue_row"."state",
  1.1564 +            NEW."initiative_id", NEW."suggestion_id",
  1.1565 +            NEW."fulfilled", NEW."degree"
  1.1566 +          );
  1.1567 +      END IF;
  1.1568 +      RETURN NULL;
  1.1569 +    END;
  1.1570 +  $$;
  1.1571 +
  1.1572 +CREATE TRIGGER "write_event_suggestion_rated"
  1.1573 +  AFTER INSERT OR UPDATE OR DELETE ON "opinion" FOR EACH ROW EXECUTE PROCEDURE
  1.1574 +  "write_event_suggestion_rated_trigger"();
  1.1575 +
  1.1576 +COMMENT ON FUNCTION "write_event_suggestion_rated_trigger"()   IS 'Implementation of trigger "write_event_suggestion_rated" on table "opinion"';
  1.1577 +COMMENT ON TRIGGER "write_event_suggestion_rated" ON "opinion" IS 'Create entry in "event" table when adding, updating, or removing support';
  1.1578 +
  1.1579 +
  1.1580 +CREATE FUNCTION "write_event_delegation_trigger"()
  1.1581 +  RETURNS TRIGGER
  1.1582 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1583 +    DECLARE
  1.1584 +      "issue_row" "issue"%ROWTYPE;
  1.1585 +      "area_row"  "area"%ROWTYPE;
  1.1586 +    BEGIN
  1.1587 +      IF TG_OP = 'DELETE' THEN
  1.1588 +        IF EXISTS (
  1.1589 +          SELECT NULL FROM "member" WHERE "id" = OLD."truster_id"
  1.1590 +        ) AND (CASE OLD."scope"
  1.1591 +          WHEN 'unit'::"delegation_scope" THEN EXISTS (
  1.1592 +            SELECT NULL FROM "unit" WHERE "id" = OLD."unit_id"
  1.1593 +          )
  1.1594 +          WHEN 'area'::"delegation_scope" THEN EXISTS (
  1.1595 +            SELECT NULL FROM "area" WHERE "id" = OLD."area_id"
  1.1596 +          )
  1.1597 +          WHEN 'issue'::"delegation_scope" THEN EXISTS (
  1.1598 +            SELECT NULL FROM "issue" WHERE "id" = OLD."issue_id"
  1.1599 +          )
  1.1600 +        END) THEN
  1.1601 +          SELECT * INTO "issue_row" FROM "issue"
  1.1602 +            WHERE "id" = OLD."issue_id" FOR SHARE;
  1.1603 +          SELECT * INTO "area_row" FROM "area"
  1.1604 +            WHERE "id" = COALESCE(OLD."area_id", "issue_row"."area_id")
  1.1605 +            FOR SHARE;
  1.1606 +          INSERT INTO "event" (
  1.1607 +              "event", "member_id", "scope",
  1.1608 +              "unit_id", "area_id", "issue_id", "state",
  1.1609 +              "boolean_value"
  1.1610 +            ) VALUES (
  1.1611 +              'delegation', OLD."truster_id", OLD."scope",
  1.1612 +              COALESCE(OLD."unit_id", "area_row"."unit_id"), "area_row"."id",
  1.1613 +              OLD."issue_id", "issue_row"."state",
  1.1614 +              FALSE
  1.1615 +            );
  1.1616 +        END IF;
  1.1617 +      ELSE
  1.1618 +        SELECT * INTO "issue_row" FROM "issue"
  1.1619 +          WHERE "id" = NEW."issue_id" FOR SHARE;
  1.1620 +        SELECT * INTO "area_row" FROM "area"
  1.1621 +          WHERE "id" = COALESCE(NEW."area_id", "issue_row"."area_id")
  1.1622 +          FOR SHARE;
  1.1623 +        INSERT INTO "event" (
  1.1624 +            "event", "member_id", "other_member_id", "scope",
  1.1625 +            "unit_id", "area_id", "issue_id", "state",
  1.1626 +            "boolean_value"
  1.1627 +          ) VALUES (
  1.1628 +            'delegation', NEW."truster_id", NEW."trustee_id", NEW."scope",
  1.1629 +            COALESCE(NEW."unit_id", "area_row"."unit_id"), "area_row"."id",
  1.1630 +            NEW."issue_id", "issue_row"."state",
  1.1631 +            TRUE
  1.1632 +          );
  1.1633 +      END IF;
  1.1634 +      RETURN NULL;
  1.1635 +    END;
  1.1636 +  $$;
  1.1637 +
  1.1638 +CREATE TRIGGER "write_event_delegation"
  1.1639 +  AFTER INSERT OR UPDATE OR DELETE ON "delegation" FOR EACH ROW EXECUTE PROCEDURE
  1.1640 +  "write_event_delegation_trigger"();
  1.1641 +
  1.1642 +COMMENT ON FUNCTION "write_event_delegation_trigger"()      IS 'Implementation of trigger "write_event_delegation" on table "delegation"';
  1.1643 +COMMENT ON TRIGGER "write_event_delegation" ON "delegation" IS 'Create entry in "event" table when adding, updating, or removing a delegation';
  1.1644 +
  1.1645 +
  1.1646 +CREATE FUNCTION "write_event_contact_trigger"()
  1.1647 +  RETURNS TRIGGER
  1.1648 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1649 +    BEGIN
  1.1650 +      IF TG_OP = 'UPDATE' THEN
  1.1651 +        IF
  1.1652 +          OLD."member_id"       = NEW."member_id" AND
  1.1653 +          OLD."other_member_id" = NEW."other_member_id" AND
  1.1654 +          OLD."public"          = NEW."public"
  1.1655 +        THEN
  1.1656 +          RETURN NULL;
  1.1657 +        END IF;
  1.1658 +      END IF;
  1.1659 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  1.1660 +        IF OLD."public" THEN
  1.1661 +          IF EXISTS (
  1.1662 +            SELECT NULL FROM "member" WHERE "id" = OLD."member_id"
  1.1663 +            FOR SHARE
  1.1664 +          ) AND EXISTS (
  1.1665 +            SELECT NULL FROM "member" WHERE "id" = OLD."other_member_id"
  1.1666 +            FOR SHARE
  1.1667 +          ) THEN
  1.1668 +            INSERT INTO "event" (
  1.1669 +                "event", "member_id", "other_member_id", "boolean_value"
  1.1670 +              ) VALUES (
  1.1671 +                'contact', OLD."member_id", OLD."other_member_id", FALSE
  1.1672 +              );
  1.1673 +          END IF;
  1.1674 +        END IF;
  1.1675 +      END IF;
  1.1676 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  1.1677 +        IF NEW."public" THEN
  1.1678 +          INSERT INTO "event" (
  1.1679 +              "event", "member_id", "other_member_id", "boolean_value"
  1.1680 +            ) VALUES (
  1.1681 +              'contact', NEW."member_id", NEW."other_member_id", TRUE
  1.1682 +            );
  1.1683 +        END IF;
  1.1684 +      END IF;
  1.1685 +      RETURN NULL;
  1.1686 +    END;
  1.1687 +  $$;
  1.1688 +
  1.1689 +CREATE TRIGGER "write_event_contact"
  1.1690 +  AFTER INSERT OR UPDATE OR DELETE ON "contact" FOR EACH ROW EXECUTE PROCEDURE
  1.1691 +  "write_event_contact_trigger"();
  1.1692 +
  1.1693 +COMMENT ON FUNCTION "write_event_contact_trigger"()   IS 'Implementation of trigger "write_event_contact" on table "contact"';
  1.1694 +COMMENT ON TRIGGER "write_event_contact" ON "contact" IS 'Create entry in "event" table when adding or removing public contacts';
  1.1695 +
  1.1696 +
  1.1697 +CREATE FUNCTION "send_event_notify_trigger"()
  1.1698 +  RETURNS TRIGGER
  1.1699 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1700 +    BEGIN
  1.1701 +      EXECUTE 'NOTIFY "event", ''' || NEW."event" || '''';
  1.1702 +      RETURN NULL;
  1.1703 +    END;
  1.1704 +  $$;
  1.1705 +
  1.1706 +CREATE TRIGGER "send_notify"
  1.1707 +  AFTER INSERT OR UPDATE ON "event" FOR EACH ROW EXECUTE PROCEDURE
  1.1708 +  "send_event_notify_trigger"();
  1.1709 +
  1.1710 +
  1.1711  
  1.1712  ----------------------------
  1.1713  -- Additional constraints --
  1.1714  ----------------------------
  1.1715  
  1.1716  
  1.1717 +CREATE FUNCTION "delete_extended_scope_tokens_trigger"()
  1.1718 +  RETURNS TRIGGER
  1.1719 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1720 +    DECLARE
  1.1721 +      "system_application_row" "system_application"%ROWTYPE;
  1.1722 +    BEGIN
  1.1723 +      IF OLD."system_application_id" NOTNULL THEN
  1.1724 +        SELECT * FROM "system_application" INTO "system_application_row"
  1.1725 +          WHERE "id" = OLD."system_application_id";
  1.1726 +        DELETE FROM "token"
  1.1727 +          WHERE "member_id" = OLD."member_id"
  1.1728 +          AND "system_application_id" = OLD."system_application_id"
  1.1729 +          AND NOT COALESCE(
  1.1730 +            regexp_split_to_array("scope", E'\\s+') <@
  1.1731 +            regexp_split_to_array(
  1.1732 +              "system_application_row"."automatic_scope", E'\\s+'
  1.1733 +            ),
  1.1734 +            FALSE
  1.1735 +          );
  1.1736 +      END IF;
  1.1737 +      RETURN OLD;
  1.1738 +    END;
  1.1739 +  $$;
  1.1740 +
  1.1741 +CREATE TRIGGER "delete_extended_scope_tokens"
  1.1742 +  BEFORE DELETE ON "member_application" FOR EACH ROW EXECUTE PROCEDURE
  1.1743 +  "delete_extended_scope_tokens_trigger"();
  1.1744 +
  1.1745 +
  1.1746 +CREATE FUNCTION "detach_token_from_session_trigger"()
  1.1747 +  RETURNS TRIGGER
  1.1748 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1749 +    BEGIN
  1.1750 +      UPDATE "token" SET "session_id" = NULL
  1.1751 +        WHERE "session_id" = OLD."id";
  1.1752 +      RETURN OLD;
  1.1753 +    END;
  1.1754 +  $$;
  1.1755 +
  1.1756 +CREATE TRIGGER "detach_token_from_session"
  1.1757 +  BEFORE DELETE ON "session" FOR EACH ROW EXECUTE PROCEDURE
  1.1758 +  "detach_token_from_session_trigger"();
  1.1759 +
  1.1760 +
  1.1761 +CREATE FUNCTION "delete_non_detached_scope_with_session_trigger"()
  1.1762 +  RETURNS TRIGGER
  1.1763 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1764 +    BEGIN
  1.1765 +      IF NEW."session_id" ISNULL THEN
  1.1766 +        SELECT coalesce(string_agg("element", ' '), '') INTO NEW."scope"
  1.1767 +          FROM unnest(regexp_split_to_array(NEW."scope", E'\\s+')) AS "element"
  1.1768 +          WHERE "element" LIKE '%_detached';
  1.1769 +      END IF;
  1.1770 +      RETURN NEW;
  1.1771 +    END;
  1.1772 +  $$;
  1.1773 +
  1.1774 +CREATE TRIGGER "delete_non_detached_scope_with_session"
  1.1775 +  BEFORE INSERT OR UPDATE ON "token" FOR EACH ROW EXECUTE PROCEDURE
  1.1776 +  "delete_non_detached_scope_with_session_trigger"();
  1.1777 +
  1.1778 +
  1.1779 +CREATE FUNCTION "delete_token_with_empty_scope_trigger"()
  1.1780 +  RETURNS TRIGGER
  1.1781 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1782 +    BEGIN
  1.1783 +      IF NEW."scope" = '' THEN
  1.1784 +        DELETE FROM "token" WHERE "id" = NEW."id";
  1.1785 +      END IF;
  1.1786 +      RETURN NULL;
  1.1787 +    END;
  1.1788 +  $$;
  1.1789 +
  1.1790 +CREATE TRIGGER "delete_token_with_empty_scope"
  1.1791 +  AFTER INSERT OR UPDATE ON "token" FOR EACH ROW EXECUTE PROCEDURE
  1.1792 +  "delete_token_with_empty_scope_trigger"();
  1.1793 +
  1.1794 +
  1.1795  CREATE FUNCTION "issue_requires_first_initiative_trigger"()
  1.1796    RETURNS TRIGGER
  1.1797    LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1798 @@ -1774,13 +2818,21 @@
  1.1799    RETURNS TRIGGER
  1.1800    LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1801      BEGIN
  1.1802 +      IF TG_OP = 'UPDATE' THEN
  1.1803 +        IF
  1.1804 +          OLD."snapshot_id" = NEW."snapshot_id" AND
  1.1805 +          OLD."issue_id" = NEW."issue_id"
  1.1806 +        THEN
  1.1807 +          RETURN NULL;
  1.1808 +        END IF;
  1.1809 +      END IF;
  1.1810        DELETE FROM "snapshot" WHERE "id" = OLD."snapshot_id";
  1.1811        RETURN NULL;
  1.1812      END;
  1.1813    $$;
  1.1814  
  1.1815  CREATE TRIGGER "delete_snapshot_on_partial_delete"
  1.1816 -  AFTER DELETE ON "snapshot_issue"
  1.1817 +  AFTER UPDATE OR DELETE ON "snapshot_issue"
  1.1818    FOR EACH ROW EXECUTE PROCEDURE
  1.1819    "delete_snapshot_on_partial_delete_trigger"();
  1.1820  
  1.1821 @@ -1794,7 +2846,7 @@
  1.1822  ---------------------------------------------------------------
  1.1823  
  1.1824  -- NOTE: Frontends should ensure this anyway, but in case of programming
  1.1825 --- errors the following triggers ensure data integrity.
  1.1826 +--       errors the following triggers ensure data integrity.
  1.1827  
  1.1828  
  1.1829  CREATE FUNCTION "forbid_changes_on_closed_issue_trigger"()
  1.1830 @@ -1871,25 +2923,6 @@
  1.1831  --------------------------------------------------------------------
  1.1832  
  1.1833  
  1.1834 -CREATE FUNCTION "autofill_unit_id_from_admission_rule_trigger"()
  1.1835 -  RETURNS TRIGGER
  1.1836 -  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1837 -    BEGIN
  1.1838 -      IF NEW."unit_id" ISNULL THEN
  1.1839 -        SELECT "unit_id" INTO NEW."unit_id"
  1.1840 -          FROM "admission_rule" WHERE "id" = NEW."admission_rule_id";
  1.1841 -      END IF;
  1.1842 -      RETURN NEW;
  1.1843 -    END;
  1.1844 -  $$;
  1.1845 -
  1.1846 -CREATE TRIGGER "autofill_unit_id" BEFORE INSERT ON "admission_rule_condition"
  1.1847 -  FOR EACH ROW EXECUTE PROCEDURE "autofill_unit_id_from_admission_rule_trigger"();
  1.1848 -
  1.1849 -COMMENT ON FUNCTION "autofill_unit_id_from_admission_rule_trigger"() IS 'Implementation of trigger "autofill_admission_rule_id" on table "admission_rule_entry"';
  1.1850 -COMMENT ON TRIGGER "autofill_unit_id" ON "admission_rule_condition"  IS 'Set "unit_id" field automatically, if NULL';
  1.1851 -
  1.1852 -
  1.1853  CREATE FUNCTION "autofill_issue_id_trigger"()
  1.1854    RETURNS TRIGGER
  1.1855    LANGUAGE 'plpgsql' VOLATILE AS $$
  1.1856 @@ -1946,8 +2979,7 @@
  1.1857        PERFORM NULL FROM "initiative" WHERE "id" = "initiative_id_p"
  1.1858          FOR UPDATE;
  1.1859        UPDATE "initiative" SET
  1.1860 -        "location1"  = "draft"."location1",
  1.1861 -        "location2"  = "draft"."location2",
  1.1862 +        "location" = "draft"."location",
  1.1863          "draft_text_search_data" = "draft"."text_search_data"
  1.1864          FROM "current_draft" AS "draft"
  1.1865          WHERE "initiative"."id" = "initiative_id_p"
  1.1866 @@ -2129,71 +3161,72 @@
  1.1867  COMMENT ON VIEW "member_to_notify" IS 'Filtered "member" table containing only members that are eligible to and wish to receive notifications; NOTE: "notify_email" may still be NULL and might need to be checked by frontend (this allows other means of messaging)';
  1.1868  
  1.1869  
  1.1870 -CREATE VIEW "matching_admission_rule_condition" AS
  1.1871 -  SELECT DISTINCT ON ("issue_id", "admission_rule_condition"."admission_rule_id")
  1.1872 -    "issue"."id" AS "issue_id",
  1.1873 -    "admission_rule_condition".*
  1.1874 -  FROM "admission_rule_condition"
  1.1875 -  JOIN "area" ON "admission_rule_condition"."unit_id" = "area"."unit_id"
  1.1876 -  JOIN "issue" ON "area"."id" = "issue"."area_id"
  1.1877 -  WHERE (
  1.1878 -    "admission_rule_condition"."policy_id" ISNULL OR
  1.1879 -    "admission_rule_condition"."policy_id" = "issue"."policy_id"
  1.1880 -  ) AND (
  1.1881 -    "admission_rule_condition"."area_id" ISNULL OR
  1.1882 -    "admission_rule_condition"."area_id" = "area"."id"
  1.1883 -  )
  1.1884 -  ORDER BY
  1.1885 -    "issue_id",
  1.1886 -    "admission_rule_condition"."admission_rule_id",
  1.1887 -    "admission_rule_condition"."policy_id" ISNULL,
  1.1888 -    "admission_rule_condition"."area_id" ISNULL;
  1.1889 -
  1.1890 -COMMENT ON VIEW "matching_admission_rule_condition" IS 'Selects the most fitting "admission_rule_condition" for a given pair of "issue" and "admission_rule"';
  1.1891 -
  1.1892 -
  1.1893 -CREATE VIEW "applicable_admission_rule" AS
  1.1894 -  SELECT * FROM "admission_rule"
  1.1895 -  WHERE NOT EXISTS (
  1.1896 -    SELECT NULL FROM "issue"
  1.1897 -    JOIN "matching_admission_rule_condition" AS "condition"
  1.1898 -    ON "issue"."id" = "condition"."issue_id"
  1.1899 -    WHERE "condition"."admission_rule_id" = "admission_rule"."id"
  1.1900 -    AND "issue"."accepted" > now() - "condition"."holdoff_time"
  1.1901 -  );
  1.1902 -
  1.1903 -COMMENT ON VIEW "applicable_admission_rule" IS 'Filters "admission_rule"s which are not blocked by a recently admitted issue';
  1.1904 +CREATE VIEW "area_quorum" AS
  1.1905 +  SELECT
  1.1906 +    "area"."id" AS "area_id",
  1.1907 +    ceil(
  1.1908 +      "area"."quorum_standard"::FLOAT8 * "quorum_factor"::FLOAT8 ^ (
  1.1909 +        coalesce(
  1.1910 +          ( SELECT sum(
  1.1911 +              ( extract(epoch from "area"."quorum_time")::FLOAT8 /
  1.1912 +                extract(epoch from
  1.1913 +                  ("issue"."accepted"-"issue"."created") +
  1.1914 +                  "issue"."discussion_time" +
  1.1915 +                  "issue"."verification_time" +
  1.1916 +                  "issue"."voting_time"
  1.1917 +                )::FLOAT8
  1.1918 +              ) ^ "area"."quorum_exponent"::FLOAT8
  1.1919 +            )
  1.1920 +            FROM "issue" JOIN "policy"
  1.1921 +            ON "issue"."policy_id" = "policy"."id"
  1.1922 +            WHERE "issue"."area_id" = "area"."id"
  1.1923 +            AND "issue"."accepted" NOTNULL
  1.1924 +            AND "issue"."closed" ISNULL
  1.1925 +            AND "policy"."polling" = FALSE
  1.1926 +          )::FLOAT8, 0::FLOAT8
  1.1927 +        ) / "area"."quorum_issues"::FLOAT8 - 1::FLOAT8
  1.1928 +      ) * CASE WHEN "area"."quorum_den" ISNULL THEN 1 ELSE (
  1.1929 +        SELECT "snapshot"."population"
  1.1930 +        FROM "snapshot"
  1.1931 +        WHERE "snapshot"."area_id" = "area"."id"
  1.1932 +        AND "snapshot"."issue_id" ISNULL
  1.1933 +        ORDER BY "snapshot"."id" DESC
  1.1934 +        LIMIT 1
  1.1935 +      ) END / coalesce("area"."quorum_den", 1)
  1.1936 +
  1.1937 +    )::INT4 AS "issue_quorum"
  1.1938 +  FROM "area";
  1.1939 +
  1.1940 +COMMENT ON VIEW "area_quorum" IS 'Area-based quorum considering number of open (accepted) issues';
  1.1941 +
  1.1942 +
  1.1943 +CREATE VIEW "area_with_unaccepted_issues" AS
  1.1944 +  SELECT DISTINCT ON ("area"."id") "area".*
  1.1945 +  FROM "area" JOIN "issue" ON "area"."id" = "issue"."area_id"
  1.1946 +  WHERE "issue"."state" = 'admission';
  1.1947 +
  1.1948 +COMMENT ON VIEW "area_with_unaccepted_issues" IS 'All areas with unaccepted open issues (needed for issue admission system)';
  1.1949  
  1.1950  
  1.1951  CREATE VIEW "issue_for_admission" AS
  1.1952 -  SELECT
  1.1953 +  SELECT DISTINCT ON ("issue"."area_id")
  1.1954      "issue".*,
  1.1955      max("initiative"."supporter_count") AS "max_supporter_count"
  1.1956    FROM "issue"
  1.1957    JOIN "policy" ON "issue"."policy_id" = "policy"."id"
  1.1958    JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
  1.1959    JOIN "area" ON "issue"."area_id" = "area"."id"
  1.1960 -  JOIN "admission_rule_condition"
  1.1961 -  ON "admission_rule_condition"."unit_id" = "area"."unit_id"
  1.1962 -  AND (
  1.1963 -    "admission_rule_condition"."policy_id" ISNULL OR
  1.1964 -    "admission_rule_condition"."policy_id" = "issue"."policy_id"
  1.1965 -  )
  1.1966 -  AND (
  1.1967 -    "admission_rule_condition"."area_id" ISNULL OR
  1.1968 -    "admission_rule_condition"."area_id" = "issue"."area_id"
  1.1969 -  )
  1.1970 -  JOIN "applicable_admission_rule"
  1.1971 -  ON "admission_rule_condition"."admission_rule_id" = "applicable_admission_rule"."id"
  1.1972    WHERE "issue"."state" = 'admission'::"issue_state"
  1.1973    AND now() >= "issue"."created" + "issue"."min_admission_time"
  1.1974    AND "initiative"."supporter_count" >= "policy"."issue_quorum"
  1.1975 +  AND "initiative"."supporter_count" * "policy"."issue_quorum_den" >=
  1.1976 +      "issue"."population" * "policy"."issue_quorum_num"
  1.1977 +  AND "initiative"."supporter_count" >= "area"."issue_quorum"
  1.1978    AND "initiative"."revoked" ISNULL
  1.1979    GROUP BY "issue"."id"
  1.1980 -  ORDER BY "max_supporter_count" DESC, "issue"."id"
  1.1981 -  LIMIT 1;
  1.1982 -
  1.1983 -COMMENT ON VIEW "issue_for_admission" IS 'Contains up to 1 issue eligible to pass from ''admission'' to ''discussion'' state; needs to be recalculated after admitting the issue in this view';
  1.1984 +  ORDER BY "issue"."area_id", "max_supporter_count" DESC, "issue"."id";
  1.1985 +
  1.1986 +COMMENT ON VIEW "issue_for_admission" IS 'Contains up to 1 issue per area eligible to pass from ''admission'' to ''discussion'' state; needs to be recalculated after admitting the issue in this view';
  1.1987  
  1.1988  
  1.1989  CREATE VIEW "unit_delegation" AS
  1.1990 @@ -2275,17 +3308,24 @@
  1.1991  COMMENT ON VIEW "member_count_view" IS 'View used to update "member_count" table';
  1.1992  
  1.1993  
  1.1994 +CREATE VIEW "unit_member" AS
  1.1995 +  SELECT
  1.1996 +    "unit"."id"   AS "unit_id",
  1.1997 +    "member"."id" AS "member_id"
  1.1998 +  FROM "privilege"
  1.1999 +  JOIN "unit"   ON "unit_id"     = "privilege"."unit_id"
  1.2000 +  JOIN "member" ON "member"."id" = "privilege"."member_id"
  1.2001 +  WHERE "privilege"."voting_right" AND "member"."active";
  1.2002 +
  1.2003 +COMMENT ON VIEW "unit_member" IS 'Active members with voting right in a unit';
  1.2004 +
  1.2005 +
  1.2006  CREATE VIEW "unit_member_count" AS
  1.2007    SELECT
  1.2008      "unit"."id" AS "unit_id",
  1.2009 -    count("member"."id") AS "member_count"
  1.2010 -  FROM "unit"
  1.2011 -  LEFT JOIN "privilege"
  1.2012 -  ON "privilege"."unit_id" = "unit"."id" 
  1.2013 -  AND "privilege"."voting_right"
  1.2014 -  LEFT JOIN "member"
  1.2015 -  ON "member"."id" = "privilege"."member_id"
  1.2016 -  AND "member"."active"
  1.2017 +    count("unit_member"."member_id") AS "member_count"
  1.2018 +  FROM "unit" LEFT JOIN "unit_member"
  1.2019 +  ON "unit"."id" = "unit_member"."unit_id"
  1.2020    GROUP BY "unit"."id";
  1.2021  
  1.2022  COMMENT ON VIEW "unit_member_count" IS 'View used to update "member_count" column of "unit" table';
  1.2023 @@ -2431,12 +3471,50 @@
  1.2024    SELECT * FROM "session" WHERE now() > "expiry";
  1.2025  
  1.2026  CREATE RULE "delete" AS ON DELETE TO "expired_session" DO INSTEAD
  1.2027 -  DELETE FROM "session" WHERE "ident" = OLD."ident";
  1.2028 +  DELETE FROM "session" WHERE "id" = OLD."id";
  1.2029  
  1.2030  COMMENT ON VIEW "expired_session" IS 'View containing all expired sessions where DELETE is possible';
  1.2031  COMMENT ON RULE "delete" ON "expired_session" IS 'Rule allowing DELETE on rows in "expired_session" view, i.e. DELETE FROM "expired_session"';
  1.2032  
  1.2033  
  1.2034 +CREATE VIEW "expired_token" AS
  1.2035 +  SELECT * FROM "token" WHERE now() > "expiry" AND NOT (
  1.2036 +    "token_type" = 'authorization' AND "used" AND EXISTS (
  1.2037 +      SELECT NULL FROM "token" AS "other"
  1.2038 +      WHERE "other"."authorization_token_id" = "id" ) );
  1.2039 +
  1.2040 +CREATE RULE "delete" AS ON DELETE TO "expired_token" DO INSTEAD
  1.2041 +  DELETE FROM "token" WHERE "id" = OLD."id";
  1.2042 +
  1.2043 +COMMENT ON VIEW "expired_token" IS 'View containing all expired tokens where DELETE is possible; Note that used authorization codes must not be deleted if still referred to by other tokens';
  1.2044 +
  1.2045 +
  1.2046 +CREATE VIEW "unused_snapshot" AS
  1.2047 +  SELECT "snapshot".* FROM "snapshot"
  1.2048 +  LEFT JOIN "issue"
  1.2049 +  ON "snapshot"."id" = "issue"."latest_snapshot_id"
  1.2050 +  OR "snapshot"."id" = "issue"."admission_snapshot_id"
  1.2051 +  OR "snapshot"."id" = "issue"."half_freeze_snapshot_id"
  1.2052 +  OR "snapshot"."id" = "issue"."full_freeze_snapshot_id"
  1.2053 +  WHERE "issue"."id" ISNULL;
  1.2054 +
  1.2055 +CREATE RULE "delete" AS ON DELETE TO "unused_snapshot" DO INSTEAD
  1.2056 +  DELETE FROM "snapshot" WHERE "id" = OLD."id";
  1.2057 +
  1.2058 +COMMENT ON VIEW "unused_snapshot" IS 'Snapshots that are not referenced by any issue (either as latest snapshot or as snapshot at phase/state change)';
  1.2059 +
  1.2060 +
  1.2061 +CREATE VIEW "expired_snapshot" AS
  1.2062 +  SELECT "unused_snapshot".* FROM "unused_snapshot" CROSS JOIN "system_setting"
  1.2063 +  WHERE "unused_snapshot"."calculated" <
  1.2064 +    now() - "system_setting"."snapshot_retention";
  1.2065 +
  1.2066 +CREATE RULE "delete" AS ON DELETE TO "expired_snapshot" DO INSTEAD
  1.2067 +  DELETE FROM "snapshot" WHERE "id" = OLD."id";
  1.2068 +
  1.2069 +COMMENT ON VIEW "expired_snapshot" IS 'Contains "unused_snapshot"s that are older than "system_setting"."snapshot_retention" (for deletion)';
  1.2070 +
  1.2071 +
  1.2072  CREATE VIEW "open_issue" AS
  1.2073    SELECT * FROM "issue" WHERE "closed" ISNULL;
  1.2074  
  1.2075 @@ -2911,7 +3989,7 @@
  1.2076  COMMENT ON TYPE "delegation_chain_row" IS 'Type of rows returned by "delegation_chain" function';
  1.2077  
  1.2078  COMMENT ON COLUMN "delegation_chain_row"."index"         IS 'Index starting with 0 and counting up';
  1.2079 -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';
  1.2080 +COMMENT ON COLUMN "delegation_chain_row"."participation" IS 'In case of delegation chains for issues: interest; for area and global delegation chains: always null';
  1.2081  COMMENT ON COLUMN "delegation_chain_row"."overridden"    IS 'True, if an entry with lower index has "participation" set to true';
  1.2082  COMMENT ON COLUMN "delegation_chain_row"."scope_in"      IS 'Scope of used incoming delegation';
  1.2083  COMMENT ON COLUMN "delegation_chain_row"."scope_out"     IS 'Scope of used outgoing delegation';
  1.2084 @@ -3087,11 +4165,6 @@
  1.2085                  AND "unit_id" = "unit_id_v";
  1.2086              END IF;
  1.2087            ELSIF "scope_v" = 'area' THEN
  1.2088 -            "output_row"."participation" := EXISTS (
  1.2089 -              SELECT NULL FROM "membership"
  1.2090 -              WHERE "area_id" = "area_id_p"
  1.2091 -              AND "member_id" = "output_row"."member_id"
  1.2092 -            );
  1.2093              IF "simulate_here_v" THEN
  1.2094                IF "simulate_trustee_id_p" ISNULL THEN
  1.2095                  SELECT * INTO "delegation_row" FROM "delegation"
  1.2096 @@ -3394,18 +4467,6 @@
  1.2097          SELECT "id" INTO "last_suggestion_id_v" FROM "suggestion"
  1.2098            WHERE "suggestion"."initiative_id" = "result_row"."initiative_id"
  1.2099            ORDER BY "id" DESC LIMIT 1;
  1.2100 -        /* compatibility with PostgreSQL 9.1 */
  1.2101 -        DELETE FROM "notification_initiative_sent"
  1.2102 -          WHERE "member_id" = "recipient_id_p"
  1.2103 -          AND "initiative_id" = "result_row"."initiative_id";
  1.2104 -        INSERT INTO "notification_initiative_sent"
  1.2105 -          ("member_id", "initiative_id", "last_draft_id", "last_suggestion_id")
  1.2106 -          VALUES (
  1.2107 -            "recipient_id_p",
  1.2108 -            "result_row"."initiative_id",
  1.2109 -            "last_draft_id_v",
  1.2110 -            "last_suggestion_id_v" );
  1.2111 -        /* TODO: use alternative code below, requires PostgreSQL 9.5 or higher
  1.2112          INSERT INTO "notification_initiative_sent"
  1.2113            ("member_id", "initiative_id", "last_draft_id", "last_suggestion_id")
  1.2114            VALUES (
  1.2115 @@ -3416,7 +4477,6 @@
  1.2116            ON CONFLICT ("member_id", "initiative_id") DO UPDATE SET
  1.2117              "last_draft_id" = "last_draft_id_v",
  1.2118              "last_suggestion_id" = "last_suggestion_id_v";
  1.2119 -        */
  1.2120          RETURN NEXT "result_row";
  1.2121        END LOOP;
  1.2122        DELETE FROM "notification_initiative_sent"
  1.2123 @@ -3479,7 +4539,22 @@
  1.2124      END;
  1.2125    $$;
  1.2126  
  1.2127 -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"';
  1.2128 +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 "unit_member_count"';
  1.2129 +
  1.2130 +
  1.2131 +CREATE FUNCTION "calculate_area_quorum"()
  1.2132 +  RETURNS VOID
  1.2133 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  1.2134 +    BEGIN
  1.2135 +      PERFORM "dont_require_transaction_isolation"();
  1.2136 +      UPDATE "area" SET "issue_quorum" = "view"."issue_quorum"
  1.2137 +        FROM "area_quorum" AS "view"
  1.2138 +        WHERE "view"."area_id" = "area"."id";
  1.2139 +      RETURN;
  1.2140 +    END;
  1.2141 +  $$;
  1.2142 +
  1.2143 +COMMENT ON FUNCTION "calculate_area_quorum"() IS 'Calculate column "issue_quorum" in table "area" from view "area_quorum"';
  1.2144  
  1.2145  
  1.2146  
  1.2147 @@ -3726,20 +4801,43 @@
  1.2148  
  1.2149  
  1.2150  CREATE FUNCTION "take_snapshot"
  1.2151 -  ( "issue_id_p" "issue"."id"%TYPE )
  1.2152 +  ( "issue_id_p" "issue"."id"%TYPE,
  1.2153 +    "area_id_p"  "area"."id"%TYPE = NULL )
  1.2154    RETURNS "snapshot"."id"%TYPE
  1.2155    LANGUAGE 'plpgsql' VOLATILE AS $$
  1.2156      DECLARE
  1.2157 +      "area_id_v"     "area"."id"%TYPE;
  1.2158 +      "unit_id_v"     "unit"."id"%TYPE;
  1.2159        "snapshot_id_v" "snapshot"."id"%TYPE;
  1.2160        "issue_id_v"    "issue"."id"%TYPE;
  1.2161        "member_id_v"   "member"."id"%TYPE;
  1.2162      BEGIN
  1.2163 +      IF "issue_id_p" NOTNULL AND "area_id_p" NOTNULL THEN
  1.2164 +        RAISE EXCEPTION 'One of "issue_id_p" and "area_id_p" must be NULL';
  1.2165 +      END IF;
  1.2166        PERFORM "require_transaction_isolation"();
  1.2167 -      INSERT INTO "snapshot" DEFAULT VALUES
  1.2168 +      IF "issue_id_p" ISNULL THEN
  1.2169 +        "area_id_v" := "area_id_p";
  1.2170 +      ELSE
  1.2171 +        SELECT "area_id" INTO "area_id_v"
  1.2172 +          FROM "issue" WHERE "id" = "issue_id_p";
  1.2173 +      END IF;
  1.2174 +      SELECT "unit_id" INTO "unit_id_v" FROM "area" WHERE "id" = "area_id_p";
  1.2175 +      INSERT INTO "snapshot" ("area_id", "issue_id")
  1.2176 +        VALUES ("area_id_v", "issue_id_p")
  1.2177          RETURNING "id" INTO "snapshot_id_v";
  1.2178 +      INSERT INTO "snapshot_population" ("snapshot_id", "member_id")
  1.2179 +        SELECT "snapshot_id_v", "member_id"
  1.2180 +        FROM "unit_member" WHERE "unit_id" = "unit_id_v";
  1.2181 +      UPDATE "snapshot" SET
  1.2182 +        "population" = (
  1.2183 +          SELECT count(1) FROM "snapshot_population"
  1.2184 +          WHERE "snapshot_id" = "snapshot_id_v"
  1.2185 +        ) WHERE "id" = "snapshot_id_v";
  1.2186        FOR "issue_id_v" IN
  1.2187          SELECT "id" FROM "issue"
  1.2188          WHERE CASE WHEN "issue_id_p" ISNULL THEN
  1.2189 +          "area_id" = "area_id_p" AND
  1.2190            "state" = 'admission'
  1.2191          ELSE
  1.2192            "id" = "issue_id_p"
  1.2193 @@ -3894,8 +4992,9 @@
  1.2194    $$;
  1.2195  
  1.2196  COMMENT ON FUNCTION "take_snapshot"
  1.2197 -  ( "issue"."id"%TYPE )
  1.2198 -  IS 'This function creates a new interest/supporter snapshot of a particular issue, or, if the argument is NULL, for all issues in ''admission'' phase. It must be executed with TRANSACTION ISOLATION LEVEL REPEATABLE READ. The snapshot must later be finished by calling "finish_snapshot" for every issue.';
  1.2199 +  ( "issue"."id"%TYPE,
  1.2200 +    "area"."id"%TYPE )
  1.2201 +  IS 'This function creates a new interest/supporter snapshot of a particular issue, or, if the first argument is NULL, for all issues in ''admission'' phase of the area given as second argument. It must be executed with TRANSACTION ISOLATION LEVEL REPEATABLE READ. The snapshot must later be finished by calling "finish_snapshot" for every issue.';
  1.2202  
  1.2203  
  1.2204  CREATE FUNCTION "finish_snapshot"
  1.2205 @@ -3905,18 +5004,19 @@
  1.2206      DECLARE
  1.2207        "snapshot_id_v" "snapshot"."id"%TYPE;
  1.2208      BEGIN
  1.2209 +      -- NOTE: function does not require snapshot isolation but we don't call
  1.2210 +      --       "dont_require_snapshot_isolation" here because this function is
  1.2211 +      --       also invoked by "check_issue"
  1.2212        LOCK TABLE "snapshot" IN EXCLUSIVE MODE;
  1.2213        SELECT "id" INTO "snapshot_id_v" FROM "snapshot"
  1.2214          ORDER BY "id" DESC LIMIT 1;
  1.2215        UPDATE "issue" SET
  1.2216 +        "calculated" = "snapshot"."calculated",
  1.2217          "latest_snapshot_id" = "snapshot_id_v",
  1.2218 -        "population" = (
  1.2219 -          SELECT coalesce(sum("weight"), 0)
  1.2220 -          FROM "direct_interest_snapshot"
  1.2221 -          WHERE "snapshot_id" = "snapshot_id_v"
  1.2222 -          AND "issue_id" = "issue_id_p"
  1.2223 -        )
  1.2224 -        WHERE "id" = "issue_id_p";
  1.2225 +        "population" = "snapshot"."population"
  1.2226 +        FROM "snapshot"
  1.2227 +        WHERE "issue"."id" = "issue_id_p"
  1.2228 +        AND "snapshot"."id" = "snapshot_id_v";
  1.2229        UPDATE "initiative" SET
  1.2230          "supporter_count" = (
  1.2231            SELECT coalesce(sum("di"."weight"), 0)
  1.2232 @@ -4715,7 +5815,8 @@
  1.2233  -----------------------------
  1.2234  
  1.2235  
  1.2236 -CREATE FUNCTION "issue_admission"()
  1.2237 +CREATE FUNCTION "issue_admission"
  1.2238 +  ( "area_id_p" "area"."id"%TYPE )
  1.2239    RETURNS BOOLEAN
  1.2240    LANGUAGE 'plpgsql' VOLATILE AS $$
  1.2241      DECLARE
  1.2242 @@ -4723,7 +5824,12 @@
  1.2243      BEGIN
  1.2244        PERFORM "dont_require_transaction_isolation"();
  1.2245        LOCK TABLE "snapshot" IN EXCLUSIVE MODE;
  1.2246 -      SELECT "id" INTO "issue_id_v" FROM "issue_for_admission" LIMIT 1;
  1.2247 +      UPDATE "area" SET "issue_quorum" = "view"."issue_quorum"
  1.2248 +        FROM "area_quorum" AS "view"
  1.2249 +        WHERE "area"."id" = "view"."area_id"
  1.2250 +        AND "area"."id" = "area_id_p";
  1.2251 +      SELECT "id" INTO "issue_id_v" FROM "issue_for_admission"
  1.2252 +        WHERE "area_id" = "area_id_p";
  1.2253        IF "issue_id_v" ISNULL THEN RETURN FALSE; END IF;
  1.2254        UPDATE "issue" SET
  1.2255          "admission_snapshot_id" = "latest_snapshot_id",
  1.2256 @@ -4735,7 +5841,9 @@
  1.2257      END;
  1.2258    $$;
  1.2259  
  1.2260 -COMMENT ON FUNCTION "issue_admission"() IS 'Checks if an issue can be admitted for further discussion; returns TRUE on success in which case the function must be called again until it returns FALSE';
  1.2261 +COMMENT ON FUNCTION "issue_admission"
  1.2262 +  ( "area"."id"%TYPE )
  1.2263 +  IS 'Checks if an issue in the area can be admitted for further discussion; returns TRUE on success in which case the function must be called again until it returns FALSE';
  1.2264  
  1.2265  
  1.2266  CREATE TYPE "check_issue_persistence" AS (
  1.2267 @@ -4848,7 +5956,8 @@
  1.2268              LOOP
  1.2269                IF
  1.2270                  "initiative_row"."polling" OR (
  1.2271 -                  "initiative_row"."satisfied_supporter_count" > 0 AND
  1.2272 +                  "initiative_row"."satisfied_supporter_count" > 
  1.2273 +                  "policy_row"."initiative_quorum" AND
  1.2274                    "initiative_row"."satisfied_supporter_count" *
  1.2275                    "policy_row"."initiative_quorum_den" >=
  1.2276                    "issue_row"."population" * "policy_row"."initiative_quorum_num"
  1.2277 @@ -4971,19 +6080,24 @@
  1.2278    RETURNS VOID
  1.2279    LANGUAGE 'plpgsql' VOLATILE AS $$
  1.2280      DECLARE
  1.2281 +      "area_id_v"     "area"."id"%TYPE;
  1.2282        "snapshot_id_v" "snapshot"."id"%TYPE;
  1.2283        "issue_id_v"    "issue"."id"%TYPE;
  1.2284        "persist_v"     "check_issue_persistence";
  1.2285      BEGIN
  1.2286        RAISE WARNING 'Function "check_everything" should only be used for development and debugging purposes';
  1.2287        DELETE FROM "expired_session";
  1.2288 +      DELETE FROM "expired_token";
  1.2289 +      DELETE FROM "expired_snapshot";
  1.2290        PERFORM "check_activity"();
  1.2291        PERFORM "calculate_member_counts"();
  1.2292 -      SELECT "take_snapshot"(NULL) INTO "snapshot_id_v";
  1.2293 -      PERFORM "finish_snapshot"("issue_id") FROM "snapshot_issue"
  1.2294 -        WHERE "snapshot_id" = "snapshot_id_v";
  1.2295 -      LOOP
  1.2296 -        EXIT WHEN "issue_admission"() = FALSE;
  1.2297 +      FOR "area_id_v" IN SELECT "id" FROM "area_with_unaccepted_issues" LOOP
  1.2298 +        SELECT "take_snapshot"(NULL, "area_id_v") INTO "snapshot_id_v";
  1.2299 +        PERFORM "finish_snapshot"("issue_id") FROM "snapshot_issue"
  1.2300 +          WHERE "snapshot_id" = "snapshot_id_v";
  1.2301 +        LOOP
  1.2302 +          EXIT WHEN "issue_admission"("area_id_v") = FALSE;
  1.2303 +        END LOOP;
  1.2304        END LOOP;
  1.2305        FOR "issue_id_v" IN SELECT "id" FROM "open_issue" LOOP
  1.2306          "persist_v" := NULL;
  1.2307 @@ -4996,7 +6110,7 @@
  1.2308      END;
  1.2309    $$;
  1.2310  
  1.2311 -COMMENT ON FUNCTION "check_everything"() IS 'Amongst other regular tasks, this function performs "check_issue" for every open issue. Use this function only for development and debugging purposes, as you may run into locking and/or serialization problems in productive environments.';
  1.2312 +COMMENT ON FUNCTION "check_everything"() IS 'Amongst other regular tasks, this function performs "check_issue" for every open issue. Use this function only for development and debugging purposes, as you may run into locking and/or serialization problems in productive environments. For production, use lf_update binary instead';
  1.2313  
  1.2314  
  1.2315  
  1.2316 @@ -5024,10 +6138,6 @@
  1.2317            WHERE "issue_id" = "issue_id_p";
  1.2318          DELETE FROM "direct_interest_snapshot"
  1.2319            WHERE "issue_id" = "issue_id_p";
  1.2320 -        DELETE FROM "delegating_population_snapshot"
  1.2321 -          WHERE "issue_id" = "issue_id_p";
  1.2322 -        DELETE FROM "direct_population_snapshot"
  1.2323 -          WHERE "issue_id" = "issue_id_p";
  1.2324          DELETE FROM "non_voter"
  1.2325            WHERE "issue_id" = "issue_id_p";
  1.2326          DELETE FROM "delegation"
  1.2327 @@ -5076,20 +6186,7 @@
  1.2328          "login_recovery_expiry"        = NULL,
  1.2329          "password_reset_secret"        = NULL,
  1.2330          "password_reset_secret_expiry" = NULL,
  1.2331 -        "organizational_unit"          = NULL,
  1.2332 -        "internal_posts"               = NULL,
  1.2333 -        "realname"                     = NULL,
  1.2334 -        "birthday"                     = NULL,
  1.2335 -        "address"                      = NULL,
  1.2336 -        "email"                        = NULL,
  1.2337 -        "xmpp_address"                 = NULL,
  1.2338 -        "website"                      = NULL,
  1.2339 -        "phone"                        = NULL,
  1.2340 -        "mobile_phone"                 = NULL,
  1.2341 -        "profession"                   = NULL,
  1.2342 -        "external_memberships"         = NULL,
  1.2343 -        "external_posts"               = NULL,
  1.2344 -        "statement"                    = NULL
  1.2345 +        "location"                     = NULL
  1.2346          WHERE "id" = "member_id_p";
  1.2347        -- "text_search_data" is updated by triggers
  1.2348        DELETE FROM "setting"            WHERE "member_id" = "member_id_p";
  1.2349 @@ -5104,7 +6201,6 @@
  1.2350        DELETE FROM "ignored_initiative" WHERE "member_id" = "member_id_p";
  1.2351        DELETE FROM "initiative_setting" WHERE "member_id" = "member_id_p";
  1.2352        DELETE FROM "suggestion_setting" WHERE "member_id" = "member_id_p";
  1.2353 -      DELETE FROM "membership"         WHERE "member_id" = "member_id_p";
  1.2354        DELETE FROM "delegation"         WHERE "truster_id" = "member_id_p";
  1.2355        DELETE FROM "non_voter"          WHERE "member_id" = "member_id_p";
  1.2356        DELETE FROM "direct_voter" USING "issue"
  1.2357 @@ -5149,21 +6245,7 @@
  1.2358          "login_recovery_expiry"        = NULL,
  1.2359          "password_reset_secret"        = NULL,
  1.2360          "password_reset_secret_expiry" = NULL,
  1.2361 -        "organizational_unit"          = NULL,
  1.2362 -        "internal_posts"               = NULL,
  1.2363 -        "realname"                     = NULL,
  1.2364 -        "birthday"                     = NULL,
  1.2365 -        "address"                      = NULL,
  1.2366 -        "email"                        = NULL,
  1.2367 -        "xmpp_address"                 = NULL,
  1.2368 -        "website"                      = NULL,
  1.2369 -        "phone"                        = NULL,
  1.2370 -        "mobile_phone"                 = NULL,
  1.2371 -        "profession"                   = NULL,
  1.2372 -        "external_memberships"         = NULL,
  1.2373 -        "external_posts"               = NULL,
  1.2374 -        "formatting_engine"            = NULL,
  1.2375 -        "statement"                    = NULL;
  1.2376 +        "location"                     = NULL;
  1.2377        -- "text_search_data" is updated by triggers
  1.2378        DELETE FROM "setting";
  1.2379        DELETE FROM "setting_map";
     2.1 --- a/lf_update.c	Sun Aug 21 17:31:44 2016 +0200
     2.2 +++ b/lf_update.c	Thu Mar 30 19:42:38 2017 +0200
     2.3 @@ -1,33 +1,51 @@
     2.4  #include <stdlib.h>
     2.5  #include <stdio.h>
     2.6  #include <string.h>
     2.7 +#include <stdint.h>
     2.8  #include <libpq-fe.h>
     2.9  
    2.10 -static char *escapeLiteral(PGconn *conn, const char *str, size_t len) {
    2.11 -  // provides compatibility for PostgreSQL versions prior 9.0
    2.12 -  // in future: return PQescapeLiteral(conn, str, len);
    2.13 -  char *res;
    2.14 -  size_t res_len;
    2.15 -  res = malloc(2*len+3);
    2.16 -  if (!res) return NULL;
    2.17 -  res[0] = '\'';
    2.18 -  res_len = PQescapeStringConn(conn, res+1, str, len, NULL);
    2.19 -  res[res_len+1] = '\'';
    2.20 -  res[res_len+2] = 0;
    2.21 -  return res;
    2.22 -}
    2.23 +#define exec_sql_error(message) do { \
    2.24 +    fprintf(stderr, message ": %s\n%s", command, PQresultErrorMessage(res)); \
    2.25 +    goto exec_sql_error_clear; \
    2.26 +  } while (0)
    2.27  
    2.28 -static void freemem(void *ptr) {
    2.29 -  // to be used for "escapeLiteral" function
    2.30 -  // provides compatibility for PostgreSQL versions prior 9.0
    2.31 -  // in future: PQfreemem(ptr);
    2.32 -  free(ptr);
    2.33 +int exec_sql(PGconn *db, PGresult **resptr, int *errptr, int onerow, char *command) {
    2.34 +  int count = 0;
    2.35 +  PGresult *res = PQexec(db, command);
    2.36 +  if (!res) {
    2.37 +    fprintf(stderr, "Error in pqlib while sending the following SQL command: %s\n", command);
    2.38 +    goto exec_sql_error_exit;
    2.39 +  }
    2.40 +  if (
    2.41 +    PQresultStatus(res) != PGRES_COMMAND_OK &&
    2.42 +    PQresultStatus(res) != PGRES_TUPLES_OK
    2.43 +  ) exec_sql_error("Error while executing the following SQL command");
    2.44 +  if (resptr) {
    2.45 +    if (PQresultStatus(res) != PGRES_TUPLES_OK) exec_sql_error("The following SQL command returned no result");
    2.46 +    count = PQntuples(res);
    2.47 +    if (count < 0) exec_sql_error("The following SQL command returned too many rows");
    2.48 +    if (onerow) {
    2.49 +      if      (count < 1) exec_sql_error("The following SQL command returned less than one row");
    2.50 +      else if (count > 1) exec_sql_error("The following SQL command returned more than one row");
    2.51 +    }
    2.52 +    *resptr = res;
    2.53 +  } else {
    2.54 +    PQclear(res);
    2.55 +  }
    2.56 +  return count;
    2.57 +  exec_sql_error_clear:
    2.58 +  PQclear(res);
    2.59 +  exec_sql_error_exit:
    2.60 +  if (resptr) *resptr = NULL;
    2.61 +  if (errptr) *errptr = 1;
    2.62 +  return -1;
    2.63  }
    2.64  
    2.65  int main(int argc, char **argv) {
    2.66  
    2.67    // variable declarations:
    2.68 -  int err = 0;
    2.69 +  int err = 0;               /* set to 1 if any error occured */
    2.70 +  int admission_failed = 0;  /* set to 1 if error occurred during admission */
    2.71    int i, count;
    2.72    char *conninfo;
    2.73    PGconn *db;
    2.74 @@ -42,15 +60,22 @@
    2.75      fprintf(out, "Usage: %s <conninfo>\n", argv[0]);
    2.76      fprintf(out, "\n");
    2.77      fprintf(out, "<conninfo> is specified by PostgreSQL's libpq,\n");
    2.78 -    fprintf(out, "see http://www.postgresql.org/docs/9.1/static/libpq-connect.html\n");
    2.79 +    fprintf(out, "see http://www.postgresql.org/docs/9.6/static/libpq-connect.html\n");
    2.80      fprintf(out, "\n");
    2.81      fprintf(out, "Example: %s dbname=liquid_feedback\n", argv[0]);
    2.82      fprintf(out, "\n");
    2.83      return argc == 1 ? 1 : 0;
    2.84    }
    2.85    {
    2.86 -    size_t len = 0;
    2.87 -    for (i=1; i<argc; i++) len += strlen(argv[i]) + 1;
    2.88 +    size_t len = 0, seglen;
    2.89 +    for (i=1; i<argc; i++) {
    2.90 +      seglen = strlen(argv[i]) + 1;
    2.91 +      if (seglen >= SIZE_MAX/2 || len >= SIZE_MAX/2) {
    2.92 +        fprintf(stderr, "Error: Command line arguments too long\n");
    2.93 +        return 1;
    2.94 +      }
    2.95 +      len += seglen;
    2.96 +    }
    2.97      conninfo = malloc(len * sizeof(char));
    2.98      if (!conninfo) {
    2.99        fprintf(stderr, "Error: Could not allocate memory for conninfo string\n");
   2.100 @@ -75,139 +100,179 @@
   2.101    }
   2.102  
   2.103    // delete expired sessions:
   2.104 -  res = PQexec(db, "DELETE FROM \"expired_session\"");
   2.105 -  if (!res) {
   2.106 -    fprintf(stderr, "Error in pqlib while sending SQL command deleting expired sessions\n");
   2.107 -    err = 1;
   2.108 -  } else if (
   2.109 -    PQresultStatus(res) != PGRES_COMMAND_OK &&
   2.110 -    PQresultStatus(res) != PGRES_TUPLES_OK
   2.111 -  ) {
   2.112 -    fprintf(stderr, "Error while executing SQL command deleting expired sessions:\n%s", PQresultErrorMessage(res));
   2.113 -    err = 1;
   2.114 -    PQclear(res);
   2.115 -  } else {
   2.116 -    PQclear(res);
   2.117 -  }
   2.118 +  exec_sql(db, NULL, &err, 0, "DELETE FROM \"expired_session\"");
   2.119 +
   2.120 +  // delete expired tokens and authorization codes:
   2.121 +  exec_sql(db, NULL, &err, 0, "DELETE FROM \"expired_token\"");
   2.122 + 
   2.123 +  // delete expired snapshots:
   2.124 +  exec_sql(db, NULL, &err, 0, "DELETE FROM \"expired_snapshot\"");
   2.125   
   2.126    // check member activity:
   2.127 -  res = PQexec(db, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT \"check_activity\"()");
   2.128 -  if (!res) {
   2.129 -    fprintf(stderr, "Error in pqlib while sending SQL command checking member activity\n");
   2.130 -    err = 1;
   2.131 -  } else if (
   2.132 -    PQresultStatus(res) != PGRES_COMMAND_OK &&
   2.133 -    PQresultStatus(res) != PGRES_TUPLES_OK
   2.134 -  ) {
   2.135 -    fprintf(stderr, "Error while executing SQL command checking member activity:\n%s", PQresultErrorMessage(res));
   2.136 -    err = 1;
   2.137 -    PQclear(res);
   2.138 -  } else {
   2.139 -    PQclear(res);
   2.140 -  }
   2.141 +  exec_sql(db, NULL, &err, 0, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT \"check_activity\"()");
   2.142  
   2.143    // calculate member counts:
   2.144 -  res = PQexec(db, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT \"calculate_member_counts\"()");
   2.145 -  if (!res) {
   2.146 -    fprintf(stderr, "Error in pqlib while sending SQL command calculating member counts\n");
   2.147 -    err = 1;
   2.148 -  } else if (
   2.149 -    PQresultStatus(res) != PGRES_COMMAND_OK &&
   2.150 -    PQresultStatus(res) != PGRES_TUPLES_OK
   2.151 -  ) {
   2.152 -    fprintf(stderr, "Error while executing SQL command calculating member counts:\n%s", PQresultErrorMessage(res));
   2.153 -    err = 1;
   2.154 -    PQclear(res);
   2.155 -  } else {
   2.156 +  exec_sql(db, NULL, &err, 0, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT \"calculate_member_counts\"()");
   2.157 +
   2.158 +  // issue admission:
   2.159 +  count = exec_sql(db, &res, &err, 0, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT \"id\" FROM \"area_with_unaccepted_issues\"");
   2.160 +  if (!res) admission_failed = 1;
   2.161 +  else {
   2.162 +    char *area_id, *escaped_area_id, *cmd;
   2.163 +    PGresult *res2;
   2.164 +    for (i=0; i<count; i++) {
   2.165 +      area_id = PQgetvalue(res, i, 0);
   2.166 +      escaped_area_id = PQescapeLiteral(db, area_id, strlen(area_id));
   2.167 +      if (!escaped_area_id) {
   2.168 +        fprintf(stderr, "Could not escape literal in memory.\n");
   2.169 +        err = admission_failed = 1;
   2.170 +        continue;
   2.171 +      }
   2.172 +      if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT \"take_snapshot\"(NULL, %s)", escaped_area_id) < 0) {
   2.173 +        fprintf(stderr, "Could not prepare query string in memory.\n");
   2.174 +        err = admission_failed = 1;
   2.175 +        PQfreemem(escaped_area_id);
   2.176 +        continue;
   2.177 +      }
   2.178 +      exec_sql(db, &res2, &err, 1, cmd);
   2.179 +      free(cmd);
   2.180 +      if (!res2) admission_failed = 1;
   2.181 +      else {
   2.182 +        char *snapshot_id, *escaped_snapshot_id;
   2.183 +        int j, count2;
   2.184 +        snapshot_id = PQgetvalue(res, 0, 0);
   2.185 +        escaped_snapshot_id = PQescapeLiteral(db, snapshot_id, strlen(snapshot_id));
   2.186 +        PQclear(res2);
   2.187 +        if (!escaped_snapshot_id) {
   2.188 +          fprintf(stderr, "Could not escape literal in memory.\n");
   2.189 +          err = admission_failed = 1;
   2.190 +          goto area_admission_cleanup;
   2.191 +        }
   2.192 +        if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT \"issue_id\" FROM \"snapshot_issue\" WHERE \"snapshot_id\" = %s", escaped_snapshot_id) < 0) {
   2.193 +          fprintf(stderr, "Could not prepare query string in memory.\n");
   2.194 +          err = admission_failed = 1;
   2.195 +          PQfreemem(escaped_snapshot_id);
   2.196 +          goto area_admission_cleanup;
   2.197 +        }
   2.198 +        PQfreemem(escaped_snapshot_id);
   2.199 +        count2 = exec_sql(db, &res2, &err, 0, cmd);
   2.200 +        free(cmd);
   2.201 +        if (!res2) admission_failed = 1;
   2.202 +        else {
   2.203 +          char *issue_id, *escaped_issue_id;
   2.204 +          for (j=0; j<count2; j++) {
   2.205 +            issue_id = PQgetvalue(res2, j, 0);
   2.206 +            escaped_issue_id = PQescapeLiteral(db, issue_id, strlen(issue_id));
   2.207 +            if (!escaped_issue_id) {
   2.208 +              fprintf(stderr, "Could not escape literal in memory.\n");
   2.209 +              err = admission_failed = 1;
   2.210 +              continue;
   2.211 +            }
   2.212 +            if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT \"finish_snapshot\"(%s)", escaped_issue_id) < 0) {
   2.213 +              fprintf(stderr, "Could not prepare query string in memory.\n");
   2.214 +              err = admission_failed = 1;
   2.215 +              PQfreemem(escaped_issue_id);
   2.216 +              continue;
   2.217 +            }
   2.218 +            PQfreemem(escaped_issue_id);
   2.219 +            if (exec_sql(db, NULL, &err, 0, cmd) < 0) admission_failed = 1;
   2.220 +            free(cmd);
   2.221 +          }
   2.222 +          PQclear(res2);
   2.223 +        }
   2.224 +        if (!admission_failed) {
   2.225 +          if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT \"issue_admission\"(%s)", escaped_area_id) < 0) {
   2.226 +            fprintf(stderr, "Could not prepare query string in memory.\n");
   2.227 +            err = admission_failed = 1;
   2.228 +            goto area_admission_cleanup;
   2.229 +          }
   2.230 +        }
   2.231 +        while (1) {
   2.232 +          exec_sql(db, &res2, &err, 1, cmd);
   2.233 +          if (!res2) {
   2.234 +            admission_failed = 1;
   2.235 +            break;
   2.236 +          }
   2.237 +          if (PQgetvalue(res2, 0, 0)[0] != 't') {
   2.238 +            PQclear(res2);
   2.239 +            break;
   2.240 +          }
   2.241 +          PQclear(res2);
   2.242 +        }
   2.243 +      }
   2.244 +      area_admission_cleanup:
   2.245 +      PQfreemem(escaped_area_id);
   2.246 +    }
   2.247      PQclear(res);
   2.248    }
   2.249  
   2.250    // update open issues:
   2.251 -  res = PQexec(db, "SELECT \"id\" FROM \"open_issue\"");
   2.252 -  if (!res) {
   2.253 -    fprintf(stderr, "Error in pqlib while sending SQL command selecting open issues\n");
   2.254 -    err = 1;
   2.255 -  } else if (PQresultStatus(res) != PGRES_TUPLES_OK) {
   2.256 -    fprintf(stderr, "Error while executing SQL command selecting open issues:\n%s", PQresultErrorMessage(res));
   2.257 -    err = 1;
   2.258 -    PQclear(res);
   2.259 -  } else {
   2.260 -    count = PQntuples(res);
   2.261 -    for (i=0; i<count; i++) {
   2.262 -      char *issue_id, *escaped_issue_id;
   2.263 -      PGresult *res2, *old_res2;
   2.264 -      int j;
   2.265 -      issue_id = PQgetvalue(res, i, 0);
   2.266 -      escaped_issue_id = escapeLiteral(db, issue_id, strlen(issue_id));
   2.267 -      if (!escaped_issue_id) {
   2.268 -        fprintf(stderr, "Could not escape literal in memory.\n");
   2.269 +  count = exec_sql(
   2.270 +    db, &res, &err, 0,
   2.271 +    admission_failed ?
   2.272 +    "SELECT \"id\" FROM \"open_issue\" WHERE \"state\" != 'admission'::\"issue_state\"" :
   2.273 +    "SELECT \"id\" FROM \"open_issue\""
   2.274 +  );
   2.275 +  for (i=0; i<count; i++) {
   2.276 +    char *issue_id, *escaped_issue_id;
   2.277 +    PGresult *res2, *old_res2;
   2.278 +    int j;
   2.279 +    issue_id = PQgetvalue(res, i, 0);
   2.280 +    escaped_issue_id = PQescapeLiteral(db, issue_id, strlen(issue_id));
   2.281 +    if (!escaped_issue_id) {
   2.282 +      fprintf(stderr, "Could not escape literal in memory.\n");
   2.283 +      err = 1;
   2.284 +      continue;
   2.285 +    }
   2.286 +    old_res2 = NULL;
   2.287 +    for (j=0; ; j++) {
   2.288 +      if (j >= 20) {  // safety to avoid endless loops
   2.289 +        fprintf(stderr, "Function \"check_issue\"(...) returned non-null value too often.\n");
   2.290          err = 1;
   2.291 +        if (j > 0) PQclear(old_res2);
   2.292          break;
   2.293        }
   2.294 -      old_res2 = NULL;
   2.295 -      for (j=0; ; j++) {
   2.296 -        if (j >= 20) {  // safety to avoid endless loops
   2.297 -          fprintf(stderr, "Function \"check_issue\"(...) returned non-null value too often.\n");
   2.298 +      if (j == 0) {
   2.299 +        char *cmd;
   2.300 +        if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT \"check_issue\"(%s, NULL)", escaped_issue_id) < 0) {
   2.301 +          fprintf(stderr, "Could not prepare query string in memory.\n");
   2.302            err = 1;
   2.303 -          if (j > 0) PQclear(old_res2);
   2.304            break;
   2.305          }
   2.306 -        if (j == 0) {
   2.307 -          char *cmd;
   2.308 -          if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT \"check_issue\"(%s, NULL)", escaped_issue_id) < 0) {
   2.309 -            fprintf(stderr, "Could not prepare query string in memory.\n");
   2.310 -            err = 1;
   2.311 -            break;
   2.312 -          }
   2.313 -          res2 = PQexec(db, cmd);
   2.314 -          free(cmd);
   2.315 -        } else {
   2.316 -          char *persist, *escaped_persist, *cmd;
   2.317 -          persist = PQgetvalue(old_res2, 0, 0);
   2.318 -          escaped_persist = escapeLiteral(db, persist, strlen(persist));
   2.319 -          if (!escaped_persist) {
   2.320 -            fprintf(stderr, "Could not escape literal in memory.\n");
   2.321 -            err = 1;
   2.322 -            PQclear(old_res2);
   2.323 -            break;
   2.324 -          }
   2.325 -          if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT \"check_issue\"(%s, %s::\"check_issue_persistence\")", escaped_issue_id, escaped_persist) < 0) {
   2.326 -            freemem(escaped_persist);
   2.327 -            fprintf(stderr, "Could not prepare query string in memory.\n");
   2.328 -            err = 1;
   2.329 -            PQclear(old_res2);
   2.330 -            break;
   2.331 -          }
   2.332 -          freemem(escaped_persist);
   2.333 -          res2 = PQexec(db, cmd);
   2.334 -          free(cmd);
   2.335 +        exec_sql(db, &res2, &err, 1, cmd);
   2.336 +        free(cmd);
   2.337 +      } else {
   2.338 +        char *persist, *escaped_persist, *cmd;
   2.339 +        persist = PQgetvalue(old_res2, 0, 0);
   2.340 +        escaped_persist = PQescapeLiteral(db, persist, strlen(persist));
   2.341 +        if (!escaped_persist) {
   2.342 +          fprintf(stderr, "Could not escape literal in memory.\n");
   2.343 +          err = 1;
   2.344 +          PQclear(old_res2);
   2.345 +          break;
   2.346 +        }
   2.347 +        if (asprintf(&cmd, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT \"check_issue\"(%s, %s::\"check_issue_persistence\")", escaped_issue_id, escaped_persist) < 0) {
   2.348 +          PQfreemem(escaped_persist);
   2.349 +          fprintf(stderr, "Could not prepare query string in memory.\n");
   2.350 +          err = 1;
   2.351            PQclear(old_res2);
   2.352 +          break;
   2.353          }
   2.354 -        if (!res2) {
   2.355 -          fprintf(stderr, "Error in pqlib while sending SQL command to call function \"check_issue\"(...):\n");
   2.356 -          err = 1;
   2.357 -          break;
   2.358 -        } else if (
   2.359 -          PQresultStatus(res2) != PGRES_COMMAND_OK &&
   2.360 -          PQresultStatus(res2) != PGRES_TUPLES_OK
   2.361 -        ) {
   2.362 -          fprintf(stderr, "Error while calling SQL function \"check_issue\"(...):\n%s", PQresultErrorMessage(res2));
   2.363 -          err = 1;
   2.364 -          PQclear(res2);
   2.365 -          break;
   2.366 -        } else {
   2.367 -          if (PQntuples(res2) >= 1 && !PQgetisnull(res2, 0, 0)) {
   2.368 -            old_res2 = res2;
   2.369 -          } else {
   2.370 -            PQclear(res2);
   2.371 -            break;
   2.372 -          }
   2.373 -        }
   2.374 +        PQfreemem(escaped_persist);
   2.375 +        exec_sql(db, &res2, &err, 1, cmd);
   2.376 +        free(cmd);
   2.377 +        PQclear(old_res2);
   2.378        }
   2.379 -      freemem(escaped_issue_id);
   2.380 +      if (!res2) break;
   2.381 +      if (PQgetisnull(res2, 0, 0)) {
   2.382 +        PQclear(res2);
   2.383 +        break;
   2.384 +      }
   2.385 +      old_res2 = res2;
   2.386      }
   2.387 -    PQclear(res);
   2.388 +    PQfreemem(escaped_issue_id);
   2.389    }
   2.390 +  if (res) PQclear(res);
   2.391  
   2.392    // cleanup and exit:
   2.393    PQfinish(db);
     3.1 --- a/test.sql	Sun Aug 21 17:31:44 2016 +0200
     3.2 +++ b/test.sql	Thu Mar 30 19:42:38 2017 +0200
     3.3 @@ -42,16 +42,16 @@
     3.4      "discussion_time",
     3.5      "verification_time",
     3.6      "voting_time",
     3.7 -    "issue_quorum",
     3.8 -    "initiative_quorum_num", "initiative_quorum_den",
     3.9 +    "issue_quorum", "issue_quorum_num", "issue_quorum_den",
    3.10 +    "initiative_quorum", "initiative_quorum_num", "initiative_quorum_den",
    3.11      "direct_majority_num", "direct_majority_den", "direct_majority_strict",
    3.12      "no_reverse_beat_path", "no_multistage_majority"
    3.13    ) VALUES (
    3.14      1,
    3.15      'Default policy',
    3.16      '0', '1 hour', '1 hour', '1 hour', '1 hour',
    3.17 -    3,
    3.18 -    30, 100,
    3.19 +    1, 10, 100,
    3.20 +    1, 30, 100,
    3.21      1, 2, TRUE,
    3.22      TRUE, FALSE );
    3.23  
    3.24 @@ -70,11 +70,6 @@
    3.25  
    3.26  INSERT INTO "unit" ("name") VALUES ('Main');
    3.27  
    3.28 -INSERT INTO "admission_rule" ("unit_id", "name") VALUES (1, 'General admission rule');
    3.29 -
    3.30 -INSERT INTO "admission_rule_condition" ("admission_rule_id", "unit_id", "holdoff_time")
    3.31 -  VALUES (1, 1, '0 seconds');
    3.32 -
    3.33  INSERT INTO "privilege" ("unit_id", "member_id", "voting_right")
    3.34    SELECT 1 AS "unit_id", "id" AS "member_id", TRUE AS "voting_right"
    3.35    FROM "member";
    3.36 @@ -409,6 +404,9 @@
    3.37          "verification_time",
    3.38          "voting_time",
    3.39          "issue_quorum",
    3.40 +        "issue_quorum_num",
    3.41 +        "issue_quorum_den",
    3.42 +        "initiative_quorum",
    3.43          "initiative_quorum_num",
    3.44          "initiative_quorum_den"
    3.45      ) VALUES (
    3.46 @@ -421,8 +419,8 @@
    3.47          '1 second',
    3.48          '1 second',
    3.49          '1 second',
    3.50 -        1,
    3.51 -        0, 100
    3.52 +        1, 0, 100,
    3.53 +        1, 0, 100
    3.54      ), (
    3.55          1,
    3.56          TRUE,
    3.57 @@ -433,8 +431,8 @@
    3.58          '2 days',
    3.59          '1 second',
    3.60          '1 second',
    3.61 -        1,
    3.62 -        0, 100
    3.63 +        1, 0, 100,
    3.64 +        1, 0, 100
    3.65      ), (
    3.66          1,
    3.67          TRUE,
    3.68 @@ -445,8 +443,8 @@
    3.69          '5 minutes',
    3.70          '2 days',
    3.71          '1 second',
    3.72 -        1,
    3.73 -        0, 100
    3.74 +        1, 0, 100,
    3.75 +        1, 0, 100
    3.76      ), (
    3.77          1,
    3.78          TRUE,
    3.79 @@ -457,8 +455,8 @@
    3.80          '5 minutes',
    3.81          '1 second',
    3.82          '2 days',
    3.83 -        1,
    3.84 -        0, 100
    3.85 +        1, 0, 100,
    3.86 +        1, 0, 100
    3.87      );
    3.88  
    3.89  END;
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/update/core-update.v3.2.2-v4.0.0.sql	Thu Mar 30 19:42:38 2017 +0200
     4.3 @@ -0,0 +1,3361 @@
     4.4 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'suggestion_removed';
     4.5 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'member_activated';
     4.6 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'member_removed';
     4.7 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'member_active';
     4.8 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'member_name_updated';
     4.9 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'member_profile_updated';
    4.10 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'member_image_updated';
    4.11 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'interest';
    4.12 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'initiator';
    4.13 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'support';
    4.14 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'support_updated';
    4.15 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'suggestion_rated';
    4.16 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'delegation';
    4.17 +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'contact';
    4.18 +
    4.19 +
    4.20 +BEGIN;
    4.21 +
    4.22 +
    4.23 +CREATE OR REPLACE VIEW "liquid_feedback_version" AS
    4.24 +  SELECT * FROM (VALUES ('4.0-dev', 4, 0, -1))
    4.25 +  AS "subquery"("string", "major", "minor", "revision");
    4.26 +
    4.27 +
    4.28 +ALTER TABLE "system_setting" ADD COLUMN "snapshot_retention" INTERVAL;
    4.29 +
    4.30 +COMMENT ON COLUMN "system_setting"."snapshot_retention" IS 'Unreferenced snapshots are retained for the given period of time after creation; set to NULL for infinite retention.';
    4.31 + 
    4.32 + 
    4.33 +CREATE TABLE "member_profile" (
    4.34 +        "member_id"             INT4            PRIMARY KEY REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
    4.35 +        "formatting_engine"     TEXT,
    4.36 +        "statement"             TEXT,
    4.37 +        "profile"               JSONB,
    4.38 +        "profile_text_data"     TEXT,
    4.39 +        "text_search_data"      TSVECTOR );
    4.40 +CREATE INDEX "member_profile_text_search_data_idx" ON "member_profile" USING gin ("text_search_data");
    4.41 +CREATE TRIGGER "update_text_search_data"
    4.42 +  BEFORE INSERT OR UPDATE ON "member_profile"
    4.43 +  FOR EACH ROW EXECUTE PROCEDURE
    4.44 +  tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
    4.45 +    'statement', 'profile_text_data');
    4.46 +
    4.47 +COMMENT ON COLUMN "member_profile"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used for "member_profile"."statement"';
    4.48 +COMMENT ON COLUMN "member_profile"."statement"         IS 'Freely chosen text of the member for his/her profile';
    4.49 +COMMENT ON COLUMN "member_profile"."profile"           IS 'Additional profile data as JSON document';
    4.50 +COMMENT ON COLUMN "member_profile"."profile_text_data" IS 'Text data from "profile" field for full text search';
    4.51 +
    4.52 +
    4.53 +INSERT INTO "member_profile"
    4.54 +  ( "member_id", "formatting_engine", "statement", "profile")
    4.55 +  SELECT
    4.56 +    "id" AS "member_id",
    4.57 +    "formatting_engine",
    4.58 +    "statement",
    4.59 +    json_build_object(
    4.60 +      'organizational_unit', "organizational_unit",
    4.61 +      'internal_posts', "internal_posts",
    4.62 +      'realname', "realname",
    4.63 +      'birthday', to_char("birthday", 'YYYY-MM-DD'),
    4.64 +      'address', "address",
    4.65 +      'email', "email",
    4.66 +      'xmpp_address', "xmpp_address",
    4.67 +      'website', "website",
    4.68 +      'phone', "phone",
    4.69 +      'mobile_phone', "mobile_phone",
    4.70 +      'profession', "profession",
    4.71 +      'external_memberships', "external_memberships",
    4.72 +      'external_posts', "external_posts"
    4.73 +    ) AS "profile"
    4.74 +  FROM "member";
    4.75 +
    4.76 +UPDATE "member_profile" SET "profile_text_data" =
    4.77 +  coalesce(("profile"->>'organizational_unit') || ' ', '') ||
    4.78 +  coalesce(("profile"->>'internal_posts') || ' ', '') ||
    4.79 +  coalesce(("profile"->>'realname') || ' ', '') ||
    4.80 +  coalesce(("profile"->>'birthday') || ' ', '') ||
    4.81 +  coalesce(("profile"->>'address') || ' ', '') ||
    4.82 +  coalesce(("profile"->>'email') || ' ', '') ||
    4.83 +  coalesce(("profile"->>'xmpp_address') || ' ', '') ||
    4.84 +  coalesce(("profile"->>'website') || ' ', '') ||
    4.85 +  coalesce(("profile"->>'phone') || ' ', '') ||
    4.86 +  coalesce(("profile"->>'mobile_phone') || ' ', '') ||
    4.87 +  coalesce(("profile"->>'profession') || ' ', '') ||
    4.88 +  coalesce(("profile"->>'external_memberships') || ' ', '') ||
    4.89 +  coalesce(("profile"->>'external_posts') || ' ', '');
    4.90 +
    4.91 +
    4.92 +DROP VIEW "newsletter_to_send";
    4.93 +DROP VIEW "scheduled_notification_to_send";
    4.94 +DROP VIEW "member_to_notify";
    4.95 +DROP VIEW "member_eligible_to_be_notified";
    4.96 +
    4.97 +
    4.98 +ALTER TABLE "member" DROP COLUMN "organizational_unit";
    4.99 +ALTER TABLE "member" DROP COLUMN "internal_posts";
   4.100 +ALTER TABLE "member" DROP COLUMN "realname";
   4.101 +ALTER TABLE "member" DROP COLUMN "birthday";
   4.102 +ALTER TABLE "member" DROP COLUMN "address";
   4.103 +ALTER TABLE "member" DROP COLUMN "email";
   4.104 +ALTER TABLE "member" DROP COLUMN "xmpp_address";
   4.105 +ALTER TABLE "member" DROP COLUMN "website";
   4.106 +ALTER TABLE "member" DROP COLUMN "phone";
   4.107 +ALTER TABLE "member" DROP COLUMN "mobile_phone";
   4.108 +ALTER TABLE "member" DROP COLUMN "profession";
   4.109 +ALTER TABLE "member" DROP COLUMN "external_memberships";
   4.110 +ALTER TABLE "member" DROP COLUMN "external_posts";
   4.111 +ALTER TABLE "member" DROP COLUMN "formatting_engine";
   4.112 +ALTER TABLE "member" DROP COLUMN "statement";
   4.113 +
   4.114 +ALTER TABLE "member" ADD COLUMN "location" JSONB;
   4.115 +COMMENT ON COLUMN "member"."location" IS 'Geographic location on earth as GeoJSON object';
   4.116 +CREATE INDEX "member_location_idx" ON "member" USING gist ((GeoJSON_to_ecluster("location")));
   4.117 +
   4.118 +DROP TRIGGER "update_text_search_data" ON "member";
   4.119 +CREATE TRIGGER "update_text_search_data"
   4.120 +  BEFORE INSERT OR UPDATE ON "member"
   4.121 +  FOR EACH ROW EXECUTE PROCEDURE
   4.122 +  tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
   4.123 +    "name", "identification");
   4.124 + 
   4.125 +
   4.126 +CREATE VIEW "member_eligible_to_be_notified" AS
   4.127 +  SELECT * FROM "member"
   4.128 +  WHERE "activated" NOTNULL AND "locked" = FALSE;
   4.129 +
   4.130 +COMMENT ON VIEW "member_eligible_to_be_notified" IS 'Filtered "member" table containing only activated and non-locked members (used as helper view for "member_to_notify" and "newsletter_to_send")';
   4.131 +
   4.132 +
   4.133 +CREATE VIEW "member_to_notify" AS
   4.134 +  SELECT * FROM "member_eligible_to_be_notified"
   4.135 +  WHERE "disable_notifications" = FALSE;
   4.136 +
   4.137 +COMMENT ON VIEW "member_to_notify" IS 'Filtered "member" table containing only members that are eligible to and wish to receive notifications; NOTE: "notify_email" may still be NULL and might need to be checked by frontend (this allows other means of messaging)';
   4.138 +
   4.139 +
   4.140 +CREATE VIEW "scheduled_notification_to_send" AS
   4.141 +  SELECT * FROM (
   4.142 +    SELECT
   4.143 +      "id" AS "recipient_id",
   4.144 +      now() - CASE WHEN "notification_dow" ISNULL THEN
   4.145 +        ( "notification_sent"::DATE + CASE
   4.146 +          WHEN EXTRACT(HOUR FROM "notification_sent") < "notification_hour"
   4.147 +          THEN 0 ELSE 1 END
   4.148 +        )::TIMESTAMP + '1 hour'::INTERVAL * "notification_hour"
   4.149 +      ELSE
   4.150 +        ( "notification_sent"::DATE +
   4.151 +          ( 7 + "notification_dow" -
   4.152 +            EXTRACT(DOW FROM
   4.153 +              ( "notification_sent"::DATE + CASE
   4.154 +                WHEN EXTRACT(HOUR FROM "notification_sent") < "notification_hour"
   4.155 +                THEN 0 ELSE 1 END
   4.156 +              )::TIMESTAMP + '1 hour'::INTERVAL * "notification_hour"
   4.157 +            )::INTEGER
   4.158 +          ) % 7 +
   4.159 +          CASE
   4.160 +            WHEN EXTRACT(HOUR FROM "notification_sent") < "notification_hour"
   4.161 +            THEN 0 ELSE 1
   4.162 +          END
   4.163 +        )::TIMESTAMP + '1 hour'::INTERVAL * "notification_hour"
   4.164 +      END AS "pending"
   4.165 +    FROM (
   4.166 +      SELECT
   4.167 +        "id",
   4.168 +        COALESCE("notification_sent", "activated") AS "notification_sent",
   4.169 +        "notification_dow",
   4.170 +        "notification_hour"
   4.171 +      FROM "member_to_notify"
   4.172 +      WHERE "notification_hour" NOTNULL
   4.173 +    ) AS "subquery1"
   4.174 +  ) AS "subquery2"
   4.175 +  WHERE "pending" > '0'::INTERVAL;
   4.176 +
   4.177 +COMMENT ON VIEW "scheduled_notification_to_send" IS 'Set of members where a scheduled notification mail is pending';
   4.178 +
   4.179 +COMMENT ON COLUMN "scheduled_notification_to_send"."recipient_id" IS '"id" of the member who needs to receive a notification mail';
   4.180 +COMMENT ON COLUMN "scheduled_notification_to_send"."pending"      IS 'Duration for which the notification mail has already been pending';
   4.181 +
   4.182 +
   4.183 +CREATE VIEW "newsletter_to_send" AS
   4.184 +  SELECT
   4.185 +    "member"."id" AS "recipient_id",
   4.186 +    "newsletter"."id" AS "newsletter_id",
   4.187 +    "newsletter"."published"
   4.188 +  FROM "newsletter" CROSS JOIN "member_eligible_to_be_notified" AS "member"
   4.189 +  LEFT JOIN "privilege" ON
   4.190 +    "privilege"."member_id" = "member"."id" AND
   4.191 +    "privilege"."unit_id" = "newsletter"."unit_id" AND
   4.192 +    "privilege"."voting_right" = TRUE
   4.193 +  LEFT JOIN "subscription" ON
   4.194 +    "subscription"."member_id" = "member"."id" AND
   4.195 +    "subscription"."unit_id" = "newsletter"."unit_id"
   4.196 +  WHERE "newsletter"."published" <= now()
   4.197 +  AND "newsletter"."sent" ISNULL
   4.198 +  AND (
   4.199 +    "member"."disable_notifications" = FALSE OR
   4.200 +    "newsletter"."include_all_members" = TRUE )
   4.201 +  AND (
   4.202 +    "newsletter"."unit_id" ISNULL OR
   4.203 +    "privilege"."member_id" NOTNULL OR
   4.204 +    "subscription"."member_id" NOTNULL );
   4.205 +
   4.206 +COMMENT ON VIEW "newsletter_to_send" IS 'List of "newsletter_id"s for each member that are due to be sent out';
   4.207 +
   4.208 +COMMENT ON COLUMN "newsletter"."published" IS 'Timestamp when the newsletter was supposed to be sent out (can be used for ordering)';
   4.209 +
   4.210 +
   4.211 +DROP VIEW "expired_session";
   4.212 +DROP TABLE "session";
   4.213 +
   4.214 +
   4.215 +CREATE TABLE "session" (
   4.216 +        UNIQUE ("member_id", "id"),  -- index needed for foreign-key on table "token"
   4.217 +        "id"                    SERIAL8         PRIMARY KEY,
   4.218 +        "ident"                 TEXT            NOT NULL UNIQUE,
   4.219 +        "additional_secret"     TEXT,
   4.220 +        "logout_token"          TEXT,
   4.221 +        "expiry"                TIMESTAMPTZ     NOT NULL DEFAULT now() + '24 hours',
   4.222 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE SET NULL,
   4.223 +        "authority"             TEXT,
   4.224 +        "authority_uid"         TEXT,
   4.225 +        "authority_login"       TEXT,
   4.226 +        "needs_delegation_check" BOOLEAN        NOT NULL DEFAULT FALSE,
   4.227 +        "lang"                  TEXT );
   4.228 +CREATE INDEX "session_expiry_idx" ON "session" ("expiry");
   4.229 +
   4.230 +COMMENT ON TABLE "session" IS 'Sessions, i.e. for a web-frontend or API layer';
   4.231 +
   4.232 +COMMENT ON COLUMN "session"."ident"             IS 'Secret session identifier (i.e. random string)';
   4.233 +COMMENT ON COLUMN "session"."additional_secret" IS 'Additional field to store a secret, which can be used against CSRF attacks';
   4.234 +COMMENT ON COLUMN "session"."logout_token"      IS 'Optional token to authorize logout through external component';
   4.235 +COMMENT ON COLUMN "session"."member_id"         IS 'Reference to member, who is logged in';
   4.236 +COMMENT ON COLUMN "session"."authority"         IS 'Temporary store for "member"."authority" during member account creation';
   4.237 +COMMENT ON COLUMN "session"."authority_uid"     IS 'Temporary store for "member"."authority_uid" during member account creation';
   4.238 +COMMENT ON COLUMN "session"."authority_login"   IS 'Temporary store for "member"."authority_login" during member account creation';
   4.239 +COMMENT ON COLUMN "session"."needs_delegation_check" IS 'Set to TRUE, if member must perform a delegation check to proceed with login; see column "last_delegation_check" in "member" table';
   4.240 +COMMENT ON COLUMN "session"."lang"              IS 'Language code of the selected language';
   4.241 +
   4.242 +
   4.243 +CREATE TYPE "authflow" AS ENUM ('code', 'token');
   4.244 +
   4.245 +COMMENT ON TYPE "authflow" IS 'OAuth 2.0 flows: ''code'' = Authorization Code flow, ''token'' = Implicit flow';
   4.246 +
   4.247 +
   4.248 +CREATE TABLE "system_application" (
   4.249 +        "id"                    SERIAL4         PRIMARY KEY,
   4.250 +        "name"                  TEXT            NOT NULL,
   4.251 +        "client_id"             TEXT            NOT NULL UNIQUE,
   4.252 +        "default_redirect_uri"  TEXT            NOT NULL,
   4.253 +        "cert_common_name"      TEXT,
   4.254 +        "client_cred_scope"     TEXT,
   4.255 +        "flow"                  "authflow",
   4.256 +        "automatic_scope"       TEXT,
   4.257 +        "permitted_scope"       TEXT,
   4.258 +        "forbidden_scope"       TEXT );
   4.259 +
   4.260 +COMMENT ON TABLE "system_application" IS 'OAuth 2.0 clients that are registered by the system administrator';
   4.261 +
   4.262 +COMMENT ON COLUMN "system_application"."name"              IS 'Human readable name of application';
   4.263 +COMMENT ON COLUMN "system_application"."client_id"         IS 'OAuth 2.0 "client_id"';
   4.264 +COMMENT ON COLUMN "system_application"."cert_common_name"  IS 'Value for CN field of TLS client certificate';
   4.265 +COMMENT ON COLUMN "system_application"."client_cred_scope" IS 'Space-separated list of scopes; If set, Client Credentials Grant is allowed; value determines scope';
   4.266 +COMMENT ON COLUMN "system_application"."flow"              IS 'If set to ''code'' or ''token'', then Authorization Code or Implicit flow is allowed respectively';
   4.267 +COMMENT ON COLUMN "system_application"."automatic_scope"   IS 'Space-separated list of scopes; Automatically granted scope for Authorization Code or Implicit flow';
   4.268 +COMMENT ON COLUMN "system_application"."permitted_scope"   IS 'Space-separated list of scopes; If set, scope that members may grant to the application is limited to the given value';
   4.269 +COMMENT ON COLUMN "system_application"."forbidden_scope"   IS 'Space-separated list of scopes that may not be granted to the application by a member';
   4.270 +
   4.271 +
   4.272 +CREATE TABLE "system_application_redirect_uri" (
   4.273 +        PRIMARY KEY ("system_application_id", "redirect_uri"),
   4.274 +        "system_application_id" INT4            REFERENCES "system_application" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.275 +        "redirect_uri"          TEXT );
   4.276 +
   4.277 +COMMENT ON TABLE "system_application_redirect_uri" IS 'Additional OAuth 2.0 redirection endpoints, which may be selected through the "redirect_uri" GET parameter';
   4.278 +
   4.279 +
   4.280 +CREATE TABLE "dynamic_application_scope" (
   4.281 +        PRIMARY KEY ("redirect_uri", "flow", "scope"),
   4.282 +        "redirect_uri"          TEXT,
   4.283 +        "flow"                  TEXT,
   4.284 +        "scope"                 TEXT,
   4.285 +        "expiry"                TIMESTAMPTZ     NOT NULL DEFAULT now() + '24 hours' );
   4.286 +CREATE INDEX "dynamic_application_scope_redirect_uri_scope_idx" ON "dynamic_application_scope" ("redirect_uri", "flow", "scope");
   4.287 +CREATE INDEX "dynamic_application_scope_expiry_idx" ON "dynamic_application_scope" ("expiry");
   4.288 +
   4.289 +COMMENT ON TABLE "dynamic_application_scope" IS 'Dynamic OAuth 2.0 client registration data';
   4.290 +
   4.291 +COMMENT ON COLUMN "dynamic_application_scope"."redirect_uri" IS 'Redirection endpoint for which the registration has been done';
   4.292 +COMMENT ON COLUMN "dynamic_application_scope"."flow"         IS 'OAuth 2.0 flow for which the registration has been done (see also "system_application"."flow")';
   4.293 +COMMENT ON COLUMN "dynamic_application_scope"."scope"        IS 'Single scope without space characters (use multiple rows for more scopes)';
   4.294 +COMMENT ON COLUMN "dynamic_application_scope"."expiry"       IS 'Expiry unless renewed';
   4.295 +
   4.296 +
   4.297 +CREATE TABLE "member_application" (
   4.298 +        "id"                    SERIAL4         PRIMARY KEY,
   4.299 +        UNIQUE ("system_application_id", "member_id"),
   4.300 +        UNIQUE ("domain", "member_id"),
   4.301 +        "member_id"             INT4            NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.302 +        "system_application_id" INT4            REFERENCES "system_application" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.303 +        "domain"                TEXT,
   4.304 +        "session_id"            INT8,
   4.305 +        FOREIGN KEY ("member_id", "session_id") REFERENCES "session" ("member_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.306 +        "scope"                 TEXT            NOT NULL,
   4.307 +        CONSTRAINT "system_application_or_domain_but_not_both" CHECK (
   4.308 +          ("system_application_id" NOTNULL AND "domain" ISNULL) OR
   4.309 +          ("system_application_id" ISNULL AND "domain" NOTNULL) ) );
   4.310 +CREATE INDEX "member_application_member_id_idx" ON "member_application" ("member_id");
   4.311 +
   4.312 +COMMENT ON TABLE "member_application" IS 'Application authorized by a member';
   4.313 +
   4.314 +COMMENT ON COLUMN "member_application"."system_application_id" IS 'If set, then application is a system application';
   4.315 +COMMENT ON COLUMN "member_application"."domain"                IS 'If set, then application is a dynamically registered OAuth 2.0 client; value is set to client''s domain';
   4.316 +COMMENT ON COLUMN "member_application"."session_id"            IS 'If set, registration ends with session';
   4.317 +COMMENT ON COLUMN "member_application"."scope"                 IS 'Granted scope as space-separated list of strings';
   4.318 +
   4.319 +
   4.320 +CREATE TYPE "token_type" AS ENUM ('authorization', 'refresh', 'access');
   4.321 +
   4.322 +COMMENT ON TYPE "token_type" IS 'Types for entries in "token" table';
   4.323 +
   4.324 +
   4.325 +CREATE TABLE "token" (
   4.326 +        "id"                    SERIAL8         PRIMARY KEY,
   4.327 +        "token"                 TEXT            NOT NULL UNIQUE,
   4.328 +        "token_type"            "token_type"    NOT NULL,
   4.329 +        "authorization_token_id" INT8           REFERENCES "token" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.330 +        "member_id"             INT4            NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.331 +        "system_application_id" INT4            REFERENCES "system_application" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.332 +        "domain"                TEXT,
   4.333 +        FOREIGN KEY ("member_id", "domain") REFERENCES "member_application" ("member_id", "domain") ON DELETE CASCADE ON UPDATE CASCADE,
   4.334 +        "session_id"            INT8,
   4.335 +        FOREIGN KEY ("member_id", "session_id") REFERENCES "session" ("member_id", "id") ON DELETE RESTRICT ON UPDATE CASCADE,  -- NOTE: deletion through "detach_token_from_session" trigger on table "session"
   4.336 +        "redirect_uri"          TEXT,
   4.337 +        "redirect_uri_explicit" BOOLEAN,
   4.338 +        "created"               TIMESTAMPTZ     NOT NULL DEFAULT now(),
   4.339 +        "expiry"                TIMESTAMPTZ     DEFAULT now() + '1 hour',
   4.340 +        "used"                  BOOLEAN         NOT NULL DEFAULT FALSE,
   4.341 +        "scope"                 TEXT            NOT NULL,
   4.342 +        CONSTRAINT "access_token_needs_expiry"
   4.343 +          CHECK ("token_type" != 'access'::"token_type" OR "expiry" NOTNULL),
   4.344 +        CONSTRAINT "authorization_token_needs_redirect_uri"
   4.345 +          CHECK ("token_type" != 'authorization'::"token_type" OR ("redirect_uri" NOTNULL AND "redirect_uri_explicit" NOTNULL) ) );
   4.346 +CREATE INDEX "token_member_id_idx" ON "token" ("member_id");
   4.347 +CREATE INDEX "token_authorization_token_id_idx" ON "token" ("authorization_token_id");
   4.348 +CREATE INDEX "token_expiry_idx" ON "token" ("expiry");
   4.349 +
   4.350 +COMMENT ON TABLE "token" IS 'Issued OAuth 2.0 authorization codes and access/refresh tokens';
   4.351 +
   4.352 +COMMENT ON COLUMN "token"."token"                  IS 'String secret (the actual token)';
   4.353 +COMMENT ON COLUMN "token"."authorization_token_id" IS 'Reference to authorization token if tokens were originally created by Authorization Code flow (allows deletion if code is used twice)';
   4.354 +COMMENT ON COLUMN "token"."system_application_id"  IS 'If set, then application is a system application';
   4.355 +COMMENT ON COLUMN "token"."domain"                 IS 'If set, then application is a dynamically registered OAuth 2.0 client; value is set to client''s domain';
   4.356 +COMMENT ON COLUMN "token"."session_id"             IS 'If set, then token is tied to a session; Deletion of session sets value to NULL (via trigger) and removes all scopes without suffix ''_detached''';
   4.357 +COMMENT ON COLUMN "token"."redirect_uri"           IS 'Authorization codes must be bound to a specific redirect URI';
   4.358 +COMMENT ON COLUMN "token"."redirect_uri_explicit"  IS 'True if ''redirect_uri'' parameter was explicitly specified during authorization request of the Authorization Code flow (since RFC 6749 requires it to be included in the access token request in this case)';
   4.359 +COMMENT ON COLUMN "token"."expiry"                 IS 'Point in time when code or token expired; In case of "used" authorization codes, authorization code must not be deleted as long as tokens exist which refer to the authorization code';
   4.360 +COMMENT ON COLUMN "token"."used"                   IS 'Can be set to TRUE for authorization codes that have been used (enables deletion of authorization codes that were used twice)';
   4.361 +COMMENT ON COLUMN "token"."scope"                  IS 'Scope as space-separated list of strings (detached scopes are marked with ''_detached'' suffix)';
   4.362 +
   4.363 +
   4.364 +CREATE TABLE "token_scope" (
   4.365 +        PRIMARY KEY ("token_id", "index"),
   4.366 +        "token_id"              INT8            REFERENCES "token" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.367 +        "index"                 INT4,
   4.368 +        "scope"                 TEXT            NOT NULL );
   4.369 +
   4.370 +COMMENT ON TABLE "token_scope" IS 'Additional scopes for an authorization code if ''scope1'', ''scope2'', etc. parameters were used during Authorization Code flow to request several access and refresh tokens at once';
   4.371 +
   4.372 +
   4.373 +ALTER TABLE "policy" ADD COLUMN "issue_quorum" INT4 CHECK ("issue_quorum" >= 1);
   4.374 +ALTER TABLE "policy" ADD COLUMN "initiative_quorum" INT4 CHECK ("initiative_quorum" >= 1);
   4.375 +
   4.376 +UPDATE "policy" SET "issue_quorum" = 1 WHERE "issue_quorum_num" NOTNULL;
   4.377 +UPDATE "policy" SET "initiative_quorum" = 1;
   4.378 +
   4.379 +ALTER TABLE "policy" ALTER COLUMN "initiative_quorum" SET NOT NULL;
   4.380 +
   4.381 +ALTER TABLE "policy" DROP CONSTRAINT "timing";
   4.382 +ALTER TABLE "policy" DROP CONSTRAINT "issue_quorum_if_and_only_if_not_polling";
   4.383 +ALTER TABLE "policy" ADD CONSTRAINT
   4.384 +  "issue_quorum_if_and_only_if_not_polling" CHECK (
   4.385 +    "polling" = ("issue_quorum"     ISNULL) AND
   4.386 +    "polling" = ("issue_quorum_num" ISNULL) AND
   4.387 +    "polling" = ("issue_quorum_den" ISNULL)
   4.388 +  );
   4.389 +ALTER TABLE "policy" ADD CONSTRAINT
   4.390 +  "min_admission_time_smaller_than_max_admission_time" CHECK (
   4.391 +    "min_admission_time" < "max_admission_time"
   4.392 +  );
   4.393 +ALTER TABLE "policy" ADD CONSTRAINT
   4.394 +  "timing_null_or_not_null_constraints" CHECK (
   4.395 +    ( "polling" = FALSE AND
   4.396 +      "min_admission_time" NOTNULL AND "max_admission_time" NOTNULL AND
   4.397 +      "discussion_time" NOTNULL AND
   4.398 +      "verification_time" NOTNULL AND
   4.399 +      "voting_time" NOTNULL ) OR
   4.400 +    ( "polling" = TRUE AND
   4.401 +      "min_admission_time" ISNULL AND "max_admission_time" ISNULL AND
   4.402 +      "discussion_time" NOTNULL AND
   4.403 +      "verification_time" NOTNULL AND
   4.404 +      "voting_time" NOTNULL ) OR
   4.405 +    ( "polling" = TRUE AND
   4.406 +      "min_admission_time" ISNULL AND "max_admission_time" ISNULL AND
   4.407 +      "discussion_time" ISNULL AND
   4.408 +      "verification_time" ISNULL AND
   4.409 +      "voting_time" ISNULL )
   4.410 +  );
   4.411 +
   4.412 +COMMENT ON COLUMN "policy"."min_admission_time"    IS 'Minimum duration of issue state ''admission''; Minimum time an issue stays open; Note: should be considerably smaller than "max_admission_time"';
   4.413 +COMMENT ON COLUMN "policy"."issue_quorum"          IS 'Absolute number of supporters needed by an initiative to be "accepted", i.e. pass from ''admission'' to ''discussion'' state';
   4.414 +COMMENT ON COLUMN "policy"."issue_quorum_num"      IS 'Numerator of supporter quorum to be reached by an initiative to be "accepted", i.e. pass from ''admission'' to ''discussion'' state (Note: further requirements apply, see quorum columns of "area" table)';
   4.415 +COMMENT ON COLUMN "policy"."issue_quorum_den"      IS 'Denominator of supporter quorum to be reached by an initiative to be "accepted", i.e. pass from ''admission'' to ''discussion'' state (Note: further requirements apply, see quorum columns of "area" table)';
   4.416 +COMMENT ON COLUMN "policy"."initiative_quorum"     IS 'Absolute number of satisfied supporters to be reached by an initiative to be "admitted" for voting';
   4.417 +COMMENT ON COLUMN "policy"."initiative_quorum_num" IS 'Numerator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
   4.418 +COMMENT ON COLUMN "policy"."initiative_quorum_den" IS 'Denominator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
   4.419 +
   4.420 +
   4.421 +ALTER TABLE "unit" ADD COLUMN "region" JSONB;
   4.422 +
   4.423 +CREATE INDEX "unit_region_idx" ON "unit" USING gist ((GeoJSON_to_ecluster("region")));
   4.424 +
   4.425 +COMMENT ON COLUMN "unit"."member_count" IS 'Count of members as determined by column "voting_right" in table "privilege" (only active members counted)';
   4.426 +COMMENT ON COLUMN "unit"."region"       IS 'Scattered (or hollow) polygon represented as an array of polygons indicating valid coordinates for initiatives of issues with this policy';
   4.427 + 
   4.428 +
   4.429 +DROP INDEX "area_unit_id_idx";
   4.430 +ALTER TABLE "area" ADD UNIQUE ("unit_id", "id");
   4.431 +
   4.432 +ALTER TABLE "area" ADD COLUMN "quorum_standard" NUMERIC  NOT NULL DEFAULT 2 CHECK ("quorum_standard" >= 0);
   4.433 +ALTER TABLE "area" ADD COLUMN "quorum_issues"   NUMERIC  NOT NULL DEFAULT 1 CHECK ("quorum_issues" > 0);
   4.434 +ALTER TABLE "area" ADD COLUMN "quorum_time"     INTERVAL NOT NULL DEFAULT '1 day' CHECK ("quorum_time" > '0'::INTERVAL);
   4.435 +ALTER TABLE "area" ADD COLUMN "quorum_exponent" NUMERIC  NOT NULL DEFAULT 0.5 CHECK ("quorum_exponent" BETWEEN 0 AND 1);
   4.436 +ALTER TABLE "area" ADD COLUMN "quorum_factor"   NUMERIC  NOT NULL DEFAULT 2 CHECK ("quorum_factor" >= 1);
   4.437 +ALTER TABLE "area" ADD COLUMN "quorum_den"      INT4     CHECK ("quorum_den" > 0);
   4.438 +ALTER TABLE "area" ADD COLUMN "issue_quorum"    INT4;
   4.439 +ALTER TABLE "area" ADD COLUMN "region"          JSONB;
   4.440 +
   4.441 +ALTER TABLE "area" DROP COLUMN "direct_member_count";
   4.442 +ALTER TABLE "area" DROP COLUMN "member_weight";
   4.443 +
   4.444 +CREATE INDEX "area_region_idx" ON "area" USING gist ((GeoJSON_to_ecluster("region")));
   4.445 +
   4.446 +COMMENT ON COLUMN "area"."quorum_standard"    IS 'Parameter for dynamic issue quorum: default quorum';
   4.447 +COMMENT ON COLUMN "area"."quorum_issues"      IS 'Parameter for dynamic issue quorum: number of open issues for default quorum';
   4.448 +COMMENT ON COLUMN "area"."quorum_time"        IS 'Parameter for dynamic issue quorum: discussion, verification, and voting time of open issues to result in the given default quorum (open issues with shorter time will increase quorum and open issues with longer time will reduce quorum if "quorum_exponent" is greater than zero)';
   4.449 +COMMENT ON COLUMN "area"."quorum_exponent"    IS 'Parameter for dynamic issue quorum: set to zero to ignore duration of open issues, set to one to fully take duration of open issues into account; defaults to 0.5';
   4.450 +COMMENT ON COLUMN "area"."quorum_factor"      IS 'Parameter for dynamic issue quorum: factor to increase dynamic quorum when a number of "quorum_issues" issues with "quorum_time" duration of discussion, verification, and voting phase are added to the number of open admitted issues';
   4.451 +COMMENT ON COLUMN "area"."quorum_den"         IS 'Parameter for dynamic issue quorum: when set, dynamic quorum is multiplied with "issue"."population" and divided by "quorum_den" (and then rounded up)';
   4.452 +COMMENT ON COLUMN "area"."issue_quorum"       IS 'Additional dynamic issue quorum based on the number of open accepted issues; automatically calculated by function "issue_admission"';
   4.453 +COMMENT ON COLUMN "area"."external_reference" IS 'Opaque data field to store an external reference';
   4.454 +COMMENT ON COLUMN "area"."region"             IS 'Scattered (or hollow) polygon represented as an array of polygons indicating valid coordinates for initiatives of issues with this policy';
   4.455 + 
   4.456 + 
   4.457 +CREATE TABLE "snapshot" (
   4.458 +        UNIQUE ("issue_id", "id"),  -- index needed for foreign-key on table "issue"
   4.459 +        "id"                    SERIAL8         PRIMARY KEY,
   4.460 +        "calculated"            TIMESTAMPTZ     NOT NULL DEFAULT now(),
   4.461 +        "population"            INT4,
   4.462 +        "area_id"               INT4            NOT NULL REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.463 +        "issue_id"              INT4 );         -- NOTE: following (cyclic) reference is added later through ALTER command: REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE
   4.464 +
   4.465 +COMMENT ON TABLE "snapshot" IS 'Point in time when a snapshot of one or more issues (see table "snapshot_issue") and their supporter situation is taken';
   4.466 +
   4.467 + 
   4.468 +CREATE TABLE "snapshot_population" (
   4.469 +        PRIMARY KEY ("snapshot_id", "member_id"),
   4.470 +        "snapshot_id"           INT8            REFERENCES "snapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.471 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE );
   4.472 +
   4.473 +COMMENT ON TABLE "snapshot_population" IS 'Members with voting right relevant for a snapshot';
   4.474 +
   4.475 +
   4.476 +ALTER TABLE "issue" ADD UNIQUE ("area_id", "id");
   4.477 +DROP INDEX "issue_area_id_idx";
   4.478 +
   4.479 +ALTER TABLE "issue" RENAME COLUMN "snapshot" TO "calculated";
   4.480 +
   4.481 +ALTER TABLE "issue" ADD COLUMN "latest_snapshot_id"      INT8 REFERENCES "snapshot" ("id") ON DELETE RESTRICT ON UPDATE CASCADE;
   4.482 +ALTER TABLE "issue" ADD COLUMN "admission_snapshot_id"   INT8 REFERENCES "snapshot" ("id") ON DELETE SET NULL ON UPDATE CASCADE;
   4.483 +ALTER TABLE "issue" ADD COLUMN "half_freeze_snapshot_id" INT8;
   4.484 +ALTER TABLE "issue" ADD COLUMN "full_freeze_snapshot_id" INT8;
   4.485 +
   4.486 +ALTER TABLE "issue" ADD FOREIGN KEY ("id", "half_freeze_snapshot_id")
   4.487 +  REFERENCES "snapshot" ("issue_id", "id") ON DELETE RESTRICT ON UPDATE CASCADE;
   4.488 +ALTER TABLE "issue" ADD FOREIGN KEY ("id", "full_freeze_snapshot_id")
   4.489 +  REFERENCES "snapshot" ("issue_id", "id") ON DELETE RESTRICT ON UPDATE CASCADE;
   4.490 +
   4.491 +ALTER TABLE "issue" DROP CONSTRAINT "last_snapshot_on_full_freeze";
   4.492 +ALTER TABLE "issue" DROP CONSTRAINT "freeze_requires_snapshot";
   4.493 +ALTER TABLE "issue" DROP CONSTRAINT "set_both_or_none_of_snapshot_and_latest_snapshot_event";
   4.494 +
   4.495 +CREATE INDEX "issue_state_idx" ON "issue" ("state");
   4.496 +CREATE INDEX "issue_latest_snapshot_id" ON "issue" ("latest_snapshot_id");
   4.497 +CREATE INDEX "issue_admission_snapshot_id" ON "issue" ("admission_snapshot_id");
   4.498 +CREATE INDEX "issue_half_freeze_snapshot_id" ON "issue" ("half_freeze_snapshot_id");
   4.499 +CREATE INDEX "issue_full_freeze_snapshot_id" ON "issue" ("full_freeze_snapshot_id");
   4.500 +
   4.501 +COMMENT ON COLUMN "issue"."accepted"                IS 'Point in time, when the issue was accepted for further discussion (see columns "issue_quorum_num" and "issue_quorum_den" of table "policy" and quorum columns of table "area")';
   4.502 +COMMENT ON COLUMN "issue"."calculated"              IS 'Point in time, when most recent snapshot and "population" and *_count values were calculated (NOTE: value is equal to "snapshot"."calculated" of snapshot with "id"="issue"."latest_snapshot_id")';
   4.503 +COMMENT ON COLUMN "issue"."latest_snapshot_id"      IS 'Snapshot id of most recent snapshot';
   4.504 +COMMENT ON COLUMN "issue"."admission_snapshot_id"   IS 'Snapshot id when issue as accepted or canceled in admission phase';
   4.505 +COMMENT ON COLUMN "issue"."half_freeze_snapshot_id" IS 'Snapshot id at end of discussion phase';
   4.506 +COMMENT ON COLUMN "issue"."full_freeze_snapshot_id" IS 'Snapshot id at end of verification phase';
   4.507 +COMMENT ON COLUMN "issue"."population"              IS 'Count of members in "snapshot_population" table with "snapshot_id" equal to "issue"."latest_snapshot_id"';
   4.508 +
   4.509 +
   4.510 +ALTER TABLE "snapshot" ADD FOREIGN KEY ("issue_id") REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
   4.511 +
   4.512 +
   4.513 +ALTER TABLE "initiative" DROP CONSTRAINT "initiative_suggested_initiative_id_fkey";
   4.514 +ALTER TABLE "initiative" ADD FOREIGN KEY ("suggested_initiative_id") REFERENCES "initiative" ("id") ON DELETE SET NULL ON UPDATE CASCADE;
   4.515 +
   4.516 +ALTER TABLE "initiative" ADD COLUMN "location" JSONB;
   4.517 +ALTER TABLE "initiative" ADD COLUMN "draft_text_search_data" TSVECTOR;
   4.518 +
   4.519 +CREATE INDEX "initiative_location_idx" ON "initiative" USING gist ((GeoJSON_to_ecluster("location")));
   4.520 +CREATE INDEX "initiative_draft_text_search_data_idx" ON "initiative" USING gin ("draft_text_search_data");
   4.521 +
   4.522 +COMMENT ON COLUMN "initiative"."location"               IS 'Geographic location of initiative as GeoJSON object (automatically copied from most recent draft)';
   4.523 +
   4.524 +
   4.525 +ALTER TABLE "draft" ADD COLUMN "location" JSONB;
   4.526 +
   4.527 +CREATE INDEX "draft_location_idx" ON "draft" USING gist ((GeoJSON_to_ecluster("location")));
   4.528 +
   4.529 +COMMENT ON COLUMN "draft"."location" IS 'Geographic location of initiative as GeoJSON object (automatically copied to "initiative" table if draft is most recent)';
   4.530 +
   4.531 +
   4.532 +ALTER TABLE "suggestion" ADD COLUMN "location" JSONB;
   4.533 +
   4.534 +CREATE INDEX "suggestion_location_idx" ON "suggestion" USING gist ((GeoJSON_to_ecluster("location")));
   4.535 +
   4.536 +COMMENT ON COLUMN "suggestion"."location"                 IS 'Geographic location of suggestion as GeoJSON object';
   4.537 +
   4.538 +
   4.539 +CREATE TABLE "temporary_suggestion_counts" (
   4.540 +        "id"                    INT8            PRIMARY KEY, -- NOTE: no referential integrity due to performance/locking issues; REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.541 +        "minus2_unfulfilled_count" INT4         NOT NULL,
   4.542 +        "minus2_fulfilled_count"   INT4         NOT NULL,
   4.543 +        "minus1_unfulfilled_count" INT4         NOT NULL,
   4.544 +        "minus1_fulfilled_count"   INT4         NOT NULL,
   4.545 +        "plus1_unfulfilled_count"  INT4         NOT NULL,
   4.546 +        "plus1_fulfilled_count"    INT4         NOT NULL,
   4.547 +        "plus2_unfulfilled_count"  INT4         NOT NULL,
   4.548 +        "plus2_fulfilled_count"    INT4         NOT NULL );
   4.549 +
   4.550 +COMMENT ON TABLE "temporary_suggestion_counts" IS 'Holds certain calculated values (suggestion counts) temporarily until they can be copied into table "suggestion"';
   4.551 +
   4.552 +COMMENT ON COLUMN "temporary_suggestion_counts"."id"  IS 'References "suggestion" ("id") but has no referential integrity trigger associated, due to performance/locking issues';
   4.553 +
   4.554 +
   4.555 +ALTER TABLE "interest" DROP CONSTRAINT "interest_member_id_fkey";
   4.556 +ALTER TABLE "interest" ADD FOREIGN KEY ("member_id") REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE;
   4.557 +
   4.558 +
   4.559 +ALTER TABLE "initiator" DROP CONSTRAINT "initiator_member_id_fkey";
   4.560 +ALTER TABLE "initiator" ADD FOREIGN KEY ("member_id") REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE;
   4.561 +
   4.562 +
   4.563 +ALTER TABLE "delegation" DROP CONSTRAINT "delegation_trustee_id_fkey";
   4.564 +ALTER TABLE "delegation" ADD FOREIGN KEY ("trustee_id") REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE;
   4.565 +
   4.566 +
   4.567 +CREATE TABLE "snapshot_issue" (
   4.568 +        PRIMARY KEY ("snapshot_id", "issue_id"),
   4.569 +        "snapshot_id"           INT8            REFERENCES "snapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.570 +        "issue_id"              INT4            REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE );
   4.571 +CREATE INDEX "snapshot_issue_issue_id_idx" ON "snapshot_issue" ("issue_id");
   4.572 +
   4.573 +COMMENT ON TABLE "snapshot_issue" IS 'List of issues included in a snapshot';
   4.574 +
   4.575 +COMMENT ON COLUMN "snapshot_issue"."issue_id" IS 'Issue being part of the snapshot; Trigger "delete_snapshot_on_partial_delete" on "snapshot_issue" table will delete snapshot if an issue of the snapshot is deleted.';
   4.576 +
   4.577 +
   4.578 +ALTER TABLE "direct_interest_snapshot" RENAME TO "direct_interest_snapshot_old";  -- TODO!
   4.579 +ALTER INDEX "direct_interest_snapshot_pkey" RENAME TO "direct_interest_snapshot_old_pkey";
   4.580 +ALTER INDEX "direct_interest_snapshot_member_id_idx" RENAME TO "direct_interest_snapshot_old_member_id_idx";
   4.581 +
   4.582 +ALTER TABLE "delegating_interest_snapshot" RENAME TO "delegating_interest_snapshot_old";  -- TODO!
   4.583 +ALTER INDEX "delegating_interest_snapshot_pkey" RENAME TO "delegating_interest_snapshot_old_pkey";
   4.584 +ALTER INDEX "delegating_interest_snapshot_member_id_idx" RENAME TO "delegating_interest_snapshot_old_member_id_idx";
   4.585 +
   4.586 +ALTER TABLE "direct_supporter_snapshot" RENAME TO "direct_supporter_snapshot_old";  -- TODO!
   4.587 +ALTER INDEX "direct_supporter_snapshot_pkey" RENAME TO "direct_supporter_snapshot_old_pkey";
   4.588 +ALTER INDEX "direct_supporter_snapshot_member_id_idx" RENAME TO "direct_supporter_snapshot_old_member_id_idx";
   4.589 +
   4.590 +
   4.591 +CREATE TABLE "direct_interest_snapshot" (
   4.592 +        PRIMARY KEY ("snapshot_id", "issue_id", "member_id"),
   4.593 +        "snapshot_id"           INT8,
   4.594 +        "issue_id"              INT4,
   4.595 +        FOREIGN KEY ("snapshot_id", "issue_id")
   4.596 +          REFERENCES "snapshot_issue" ("snapshot_id", "issue_id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.597 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
   4.598 +        "weight"                INT4 );
   4.599 +CREATE INDEX "direct_interest_snapshot_member_id_idx" ON "direct_interest_snapshot" ("member_id");
   4.600 +
   4.601 +COMMENT ON TABLE "direct_interest_snapshot" IS 'Snapshot of active members having an "interest" in the "issue"; for corrections refer to column "issue_notice" of "issue" table';
   4.602 +
   4.603 +COMMENT ON COLUMN "direct_interest_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_interest_snapshot"';
   4.604 +
   4.605 +
   4.606 +CREATE TABLE "delegating_interest_snapshot" (
   4.607 +        PRIMARY KEY ("snapshot_id", "issue_id", "member_id"),
   4.608 +        "snapshot_id"           INT8,
   4.609 +        "issue_id"              INT4,
   4.610 +        FOREIGN KEY ("snapshot_id", "issue_id")
   4.611 +          REFERENCES "snapshot_issue" ("snapshot_id", "issue_id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.612 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
   4.613 +        "weight"                INT4,
   4.614 +        "scope"              "delegation_scope" NOT NULL,
   4.615 +        "delegate_member_ids"   INT4[]          NOT NULL );
   4.616 +CREATE INDEX "delegating_interest_snapshot_member_id_idx" ON "delegating_interest_snapshot" ("member_id");
   4.617 +
   4.618 +COMMENT ON TABLE "delegating_interest_snapshot" IS 'Delegations increasing the weight of entries in the "direct_interest_snapshot" table; for corrections refer to column "issue_notice" of "issue" table';
   4.619 +
   4.620 +COMMENT ON COLUMN "delegating_interest_snapshot"."member_id"           IS 'Delegating member';
   4.621 +COMMENT ON COLUMN "delegating_interest_snapshot"."weight"              IS 'Intermediate weight';
   4.622 +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"';
   4.623 +
   4.624 +
   4.625 +CREATE TABLE "direct_supporter_snapshot" (
   4.626 +        PRIMARY KEY ("snapshot_id", "initiative_id", "member_id"),
   4.627 +        "snapshot_id"           INT8,
   4.628 +        "issue_id"              INT4            NOT NULL,
   4.629 +        FOREIGN KEY ("snapshot_id", "issue_id")
   4.630 +          REFERENCES "snapshot_issue" ("snapshot_id", "issue_id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.631 +        "initiative_id"         INT4,
   4.632 +        "member_id"             INT4            REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
   4.633 +        "draft_id"              INT8            NOT NULL,
   4.634 +        "informed"              BOOLEAN         NOT NULL,
   4.635 +        "satisfied"             BOOLEAN         NOT NULL,
   4.636 +        FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
   4.637 +        FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE NO ACTION ON UPDATE CASCADE,
   4.638 +        FOREIGN KEY ("snapshot_id", "issue_id", "member_id") REFERENCES "direct_interest_snapshot" ("snapshot_id", "issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
   4.639 +CREATE INDEX "direct_supporter_snapshot_member_id_idx" ON "direct_supporter_snapshot" ("member_id");
   4.640 +
   4.641 +COMMENT ON TABLE "direct_supporter_snapshot" IS 'Snapshot of supporters of initiatives (weight is stored in "direct_interest_snapshot"); for corrections refer to column "issue_notice" of "issue" table';
   4.642 +
   4.643 +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';
   4.644 +COMMENT ON COLUMN "direct_supporter_snapshot"."informed"  IS 'Supporter has seen the latest draft of the initiative';
   4.645 +COMMENT ON COLUMN "direct_supporter_snapshot"."satisfied" IS 'Supporter has no "critical_opinion"s';
   4.646 + 
   4.647 +
   4.648 +ALTER TABLE "non_voter" DROP CONSTRAINT "non_voter_pkey";
   4.649 +DROP INDEX "non_voter_member_id_idx";
   4.650 +
   4.651 +ALTER TABLE "non_voter" ADD PRIMARY KEY ("member_id", "issue_id");
   4.652 +CREATE INDEX "non_voter_issue_id_idx" ON "non_voter" ("issue_id");
   4.653 +
   4.654 +
   4.655 +ALTER TABLE "event" ADD COLUMN "other_member_id" INT4    REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE;
   4.656 +ALTER TABLE "event" ADD COLUMN "scope"           "delegation_scope";
   4.657 +ALTER TABLE "event" ADD COLUMN "unit_id"         INT4    REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
   4.658 +ALTER TABLE "event" ADD COLUMN "area_id"         INT4;
   4.659 +ALTER TABLE "event" ADD COLUMN "boolean_value"   BOOLEAN;
   4.660 +ALTER TABLE "event" ADD COLUMN "numeric_value"   INT4;
   4.661 +ALTER TABLE "event" ADD COLUMN "text_value"      TEXT;
   4.662 +ALTER TABLE "event" ADD COLUMN "old_text_value"  TEXT;
   4.663 +
   4.664 +ALTER TABLE "event" ADD FOREIGN KEY ("unit_id", "area_id") REFERENCES "area" ("unit_id", "id") ON DELETE CASCADE ON UPDATE CASCADE;
   4.665 +ALTER TABLE "event" ADD FOREIGN KEY ("area_id", "issue_id") REFERENCES "issue" ("area_id", "id") ON DELETE CASCADE ON UPDATE CASCADE;
   4.666 +
   4.667 +ALTER TABLE "event" DROP CONSTRAINT "event_initiative_id_fkey1";
   4.668 +ALTER TABLE "event" DROP CONSTRAINT "null_constr_for_issue_state_changed";
   4.669 +ALTER TABLE "event" DROP CONSTRAINT "null_constr_for_initiative_creation_or_revocation_or_new_draft";
   4.670 +ALTER TABLE "event" DROP CONSTRAINT "null_constr_for_suggestion_creation";
   4.671 +
   4.672 +UPDATE "event" SET "unit_id" = "area"."unit_id", "area_id" = "issue"."area_id"
   4.673 +  FROM "issue", "area"
   4.674 +  WHERE "issue"."id" = "event"."issue_id" AND "area"."id" = "issue"."area_id";
   4.675 +
   4.676 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_issue_state_changed" CHECK (
   4.677 +          "event" != 'issue_state_changed' OR (
   4.678 +            "member_id"       ISNULL  AND
   4.679 +            "other_member_id" ISNULL  AND
   4.680 +            "scope"           ISNULL  AND
   4.681 +            "unit_id"         NOTNULL AND
   4.682 +            "area_id"         NOTNULL AND
   4.683 +            "issue_id"        NOTNULL AND
   4.684 +            "state"           NOTNULL AND
   4.685 +            "initiative_id"   ISNULL  AND
   4.686 +            "draft_id"        ISNULL  AND
   4.687 +            "suggestion_id"   ISNULL  AND
   4.688 +            "boolean_value"   ISNULL  AND
   4.689 +            "numeric_value"   ISNULL  AND
   4.690 +            "text_value"      ISNULL  AND
   4.691 +            "old_text_value"  ISNULL ));
   4.692 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_initiative_creation_or_revocation_or_new_draft" CHECK (
   4.693 +          "event" NOT IN (
   4.694 +            'initiative_created_in_new_issue',
   4.695 +            'initiative_created_in_existing_issue',
   4.696 +            'initiative_revoked',
   4.697 +            'new_draft_created'
   4.698 +          ) OR (
   4.699 +            "member_id"       NOTNULL AND
   4.700 +            "other_member_id" ISNULL  AND
   4.701 +            "scope"           ISNULL  AND
   4.702 +            "unit_id"         NOTNULL AND
   4.703 +            "area_id"         NOTNULL AND
   4.704 +            "issue_id"        NOTNULL AND
   4.705 +            "state"           NOTNULL AND
   4.706 +            "initiative_id"   NOTNULL AND
   4.707 +            "draft_id"        NOTNULL AND
   4.708 +            "suggestion_id"   ISNULL  AND
   4.709 +            "boolean_value"   ISNULL  AND
   4.710 +            "numeric_value"   ISNULL  AND
   4.711 +            "text_value"      ISNULL  AND
   4.712 +            "old_text_value"  ISNULL ));
   4.713 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_suggestion_creation" CHECK (
   4.714 +          "event" != 'suggestion_created' OR (
   4.715 +            "member_id"       NOTNULL AND
   4.716 +            "other_member_id" ISNULL  AND
   4.717 +            "scope"           ISNULL  AND
   4.718 +            "unit_id"         NOTNULL AND
   4.719 +            "area_id"         NOTNULL AND
   4.720 +            "issue_id"        NOTNULL AND
   4.721 +            "state"           NOTNULL AND
   4.722 +            "initiative_id"   NOTNULL AND
   4.723 +            "draft_id"        ISNULL  AND
   4.724 +            "suggestion_id"   NOTNULL AND
   4.725 +            "boolean_value"   ISNULL  AND
   4.726 +            "numeric_value"   ISNULL  AND
   4.727 +            "text_value"      ISNULL  AND
   4.728 +            "old_text_value"  ISNULL ));
   4.729 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_suggestion_removal" CHECK (
   4.730 +          "event" != 'suggestion_removed' OR (
   4.731 +            "member_id"       ISNULL AND
   4.732 +            "other_member_id" ISNULL  AND
   4.733 +            "scope"           ISNULL  AND
   4.734 +            "unit_id"         NOTNULL AND
   4.735 +            "area_id"         NOTNULL AND
   4.736 +            "issue_id"        NOTNULL AND
   4.737 +            "state"           NOTNULL AND
   4.738 +            "initiative_id"   NOTNULL AND
   4.739 +            "draft_id"        ISNULL  AND
   4.740 +            "suggestion_id"   NOTNULL AND
   4.741 +            "boolean_value"   ISNULL  AND
   4.742 +            "numeric_value"   ISNULL  AND
   4.743 +            "text_value"      ISNULL  AND
   4.744 +            "old_text_value"  ISNULL ));
   4.745 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_value_less_member_event" CHECK (
   4.746 +          "event" NOT IN (
   4.747 +            'member_activated',
   4.748 +            'member_removed',
   4.749 +            'member_profile_updated',
   4.750 +            'member_image_updated'
   4.751 +          ) OR (
   4.752 +            "member_id"       NOTNULL AND
   4.753 +            "other_member_id" ISNULL  AND
   4.754 +            "scope"           ISNULL  AND
   4.755 +            "unit_id"         ISNULL  AND
   4.756 +            "area_id"         ISNULL  AND
   4.757 +            "issue_id"        ISNULL  AND
   4.758 +            "state"           ISNULL  AND
   4.759 +            "initiative_id"   ISNULL  AND
   4.760 +            "draft_id"        ISNULL  AND
   4.761 +            "suggestion_id"   ISNULL  AND
   4.762 +            "boolean_value"   ISNULL  AND
   4.763 +            "numeric_value"   ISNULL  AND
   4.764 +            "text_value"      ISNULL  AND
   4.765 +            "old_text_value"  ISNULL ));
   4.766 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_member_active" CHECK (
   4.767 +          "event" != 'member_active' OR (
   4.768 +            "member_id"       NOTNULL AND
   4.769 +            "other_member_id" ISNULL  AND
   4.770 +            "scope"           ISNULL  AND
   4.771 +            "unit_id"         ISNULL  AND
   4.772 +            "area_id"         ISNULL  AND
   4.773 +            "issue_id"        ISNULL  AND
   4.774 +            "state"           ISNULL  AND
   4.775 +            "initiative_id"   ISNULL  AND
   4.776 +            "draft_id"        ISNULL  AND
   4.777 +            "suggestion_id"   ISNULL  AND
   4.778 +            "boolean_value"   NOTNULL AND
   4.779 +            "numeric_value"   ISNULL  AND
   4.780 +            "text_value"      ISNULL  AND
   4.781 +            "old_text_value"  ISNULL ));
   4.782 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_member_name_updated" CHECK (
   4.783 +          "event" != 'member_name_updated' OR (
   4.784 +            "member_id"       NOTNULL AND
   4.785 +            "other_member_id" ISNULL  AND
   4.786 +            "scope"           ISNULL  AND
   4.787 +            "unit_id"         ISNULL  AND
   4.788 +            "area_id"         ISNULL  AND
   4.789 +            "issue_id"        ISNULL  AND
   4.790 +            "state"           ISNULL  AND
   4.791 +            "initiative_id"   ISNULL  AND
   4.792 +            "draft_id"        ISNULL  AND
   4.793 +            "suggestion_id"   ISNULL  AND
   4.794 +            "boolean_value"   ISNULL  AND
   4.795 +            "numeric_value"   ISNULL  AND
   4.796 +            "text_value"      NOTNULL AND
   4.797 +            "old_text_value"  NOTNULL ));
   4.798 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_interest" CHECK (
   4.799 +          "event" != 'interest' OR (
   4.800 +            "member_id"       NOTNULL AND
   4.801 +            "other_member_id" ISNULL  AND
   4.802 +            "scope"           ISNULL  AND
   4.803 +            "unit_id"         NOTNULL AND
   4.804 +            "area_id"         NOTNULL AND
   4.805 +            "issue_id"        NOTNULL AND
   4.806 +            "state"           NOTNULL AND
   4.807 +            "initiative_id"   ISNULL  AND
   4.808 +            "draft_id"        ISNULL  AND
   4.809 +            "suggestion_id"   ISNULL  AND
   4.810 +            "boolean_value"   NOTNULL AND
   4.811 +            "numeric_value"   ISNULL  AND
   4.812 +            "text_value"      ISNULL  AND
   4.813 +            "old_text_value"  ISNULL ));
   4.814 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_initiator" CHECK (
   4.815 +          "event" != 'initiator' OR (
   4.816 +            "member_id"       NOTNULL AND
   4.817 +            "other_member_id" ISNULL  AND
   4.818 +            "scope"           ISNULL  AND
   4.819 +            "unit_id"         NOTNULL AND
   4.820 +            "area_id"         NOTNULL AND
   4.821 +            "issue_id"        NOTNULL AND
   4.822 +            "state"           NOTNULL AND
   4.823 +            "initiative_id"   NOTNULL AND
   4.824 +            "draft_id"        ISNULL  AND
   4.825 +            "suggestion_id"   ISNULL  AND
   4.826 +            "boolean_value"   NOTNULL AND
   4.827 +            "numeric_value"   ISNULL  AND
   4.828 +            "text_value"      ISNULL  AND
   4.829 +            "old_text_value"  ISNULL ));
   4.830 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_support" CHECK (
   4.831 +          "event" != 'support' OR (
   4.832 +            "member_id"       NOTNULL AND
   4.833 +            "other_member_id" ISNULL  AND
   4.834 +            "scope"           ISNULL  AND
   4.835 +            "unit_id"         NOTNULL AND
   4.836 +            "area_id"         NOTNULL AND
   4.837 +            "issue_id"        NOTNULL AND
   4.838 +            "state"           NOTNULL AND
   4.839 +            "initiative_id"   NOTNULL AND
   4.840 +            ("draft_id" NOTNULL) = ("boolean_value" = TRUE) AND
   4.841 +            "suggestion_id"   ISNULL  AND
   4.842 +            "boolean_value"   NOTNULL AND
   4.843 +            "numeric_value"   ISNULL  AND
   4.844 +            "text_value"      ISNULL  AND
   4.845 +            "old_text_value"  ISNULL ));
   4.846 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_support_updated" CHECK (
   4.847 +          "event" != 'support_updated' OR (
   4.848 +            "member_id"       NOTNULL AND
   4.849 +            "other_member_id" ISNULL  AND
   4.850 +            "scope"           ISNULL  AND
   4.851 +            "unit_id"         NOTNULL AND
   4.852 +            "area_id"         NOTNULL AND
   4.853 +            "issue_id"        NOTNULL AND
   4.854 +            "state"           NOTNULL AND
   4.855 +            "initiative_id"   NOTNULL AND
   4.856 +            "draft_id"        NOTNULL AND
   4.857 +            "suggestion_id"   ISNULL  AND
   4.858 +            "boolean_value"   ISNULL  AND
   4.859 +            "numeric_value"   ISNULL  AND
   4.860 +            "text_value"      ISNULL  AND
   4.861 +            "old_text_value"  ISNULL ));
   4.862 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_suggestion_rated" CHECK (
   4.863 +          "event" != 'suggestion_rated' OR (
   4.864 +            "member_id"       NOTNULL AND
   4.865 +            "other_member_id" ISNULL  AND
   4.866 +            "scope"           ISNULL  AND
   4.867 +            "unit_id"         NOTNULL AND
   4.868 +            "area_id"         NOTNULL AND
   4.869 +            "issue_id"        NOTNULL AND
   4.870 +            "state"           NOTNULL AND
   4.871 +            "initiative_id"   NOTNULL AND
   4.872 +            "draft_id"        ISNULL  AND
   4.873 +            "suggestion_id"   NOTNULL AND
   4.874 +            ("boolean_value" NOTNULL) = ("numeric_value" != 0) AND
   4.875 +            "numeric_value"   NOTNULL AND
   4.876 +            "numeric_value" IN (-2, -1, 0, 1, 2) AND
   4.877 +            "text_value"      ISNULL  AND
   4.878 +            "old_text_value"  ISNULL ));
   4.879 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_delegation" CHECK (
   4.880 +          "event" != 'delegation' OR (
   4.881 +            "member_id"       NOTNULL AND
   4.882 +            ("other_member_id" NOTNULL) OR ("boolean_value" = FALSE) AND
   4.883 +            "scope"           NOTNULL AND
   4.884 +            "unit_id"         NOTNULL AND
   4.885 +            ("area_id"  NOTNULL) = ("scope" != 'unit'::"delegation_scope") AND
   4.886 +            ("issue_id" NOTNULL) = ("scope" = 'issue'::"delegation_scope") AND
   4.887 +            ("state"    NOTNULL) = ("scope" = 'issue'::"delegation_scope") AND
   4.888 +            "initiative_id"   ISNULL  AND
   4.889 +            "draft_id"        ISNULL  AND
   4.890 +            "suggestion_id"   ISNULL  AND
   4.891 +            "boolean_value"   NOTNULL AND
   4.892 +            "numeric_value"   ISNULL  AND
   4.893 +            "text_value"      ISNULL  AND
   4.894 +            "old_text_value"  ISNULL ));
   4.895 +ALTER TABLE "event" ADD CONSTRAINT "constr_for_contact" CHECK (
   4.896 +          "event" != 'contact' OR (
   4.897 +            "member_id"       NOTNULL AND
   4.898 +            "other_member_id" NOTNULL AND
   4.899 +            "scope"           ISNULL  AND
   4.900 +            "unit_id"         ISNULL  AND
   4.901 +            "area_id"         ISNULL  AND
   4.902 +            "issue_id"        ISNULL  AND
   4.903 +            "state"           ISNULL  AND
   4.904 +            "initiative_id"   ISNULL  AND
   4.905 +            "draft_id"        ISNULL  AND
   4.906 +            "suggestion_id"   ISNULL  AND
   4.907 +            "boolean_value"   NOTNULL AND
   4.908 +            "numeric_value"   ISNULL  AND
   4.909 +            "text_value"      ISNULL  AND
   4.910 +            "old_text_value"  ISNULL ));
   4.911 +
   4.912 +
   4.913 +CREATE OR REPLACE FUNCTION "write_event_issue_state_changed_trigger"()
   4.914 +  RETURNS TRIGGER
   4.915 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   4.916 +    DECLARE
   4.917 +      "area_row" "area"%ROWTYPE;
   4.918 +    BEGIN
   4.919 +      IF NEW."state" != OLD."state" THEN
   4.920 +        SELECT * INTO "area_row" FROM "area" WHERE "id" = NEW."area_id"
   4.921 +          FOR SHARE;
   4.922 +        INSERT INTO "event" (
   4.923 +            "event",
   4.924 +            "unit_id", "area_id", "issue_id", "state"
   4.925 +          ) VALUES (
   4.926 +            'issue_state_changed',
   4.927 +            "area_row"."unit_id", NEW."area_id", NEW."id", NEW."state"
   4.928 +          );
   4.929 +      END IF;
   4.930 +      RETURN NULL;
   4.931 +    END;
   4.932 +  $$;
   4.933 +
   4.934 +
   4.935 +CREATE OR REPLACE FUNCTION "write_event_initiative_or_draft_created_trigger"()
   4.936 +  RETURNS TRIGGER
   4.937 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   4.938 +    DECLARE
   4.939 +      "initiative_row" "initiative"%ROWTYPE;
   4.940 +      "issue_row"      "issue"%ROWTYPE;
   4.941 +      "area_row"       "area"%ROWTYPE;
   4.942 +      "event_v"        "event_type";
   4.943 +    BEGIN
   4.944 +      SELECT * INTO "initiative_row" FROM "initiative"
   4.945 +        WHERE "id" = NEW."initiative_id" FOR SHARE;
   4.946 +      SELECT * INTO "issue_row" FROM "issue"
   4.947 +        WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
   4.948 +      SELECT * INTO "area_row" FROM "area"
   4.949 +        WHERE "id" = "issue_row"."area_id" FOR SHARE;
   4.950 +      IF EXISTS (
   4.951 +        SELECT NULL FROM "draft"
   4.952 +        WHERE "initiative_id" = NEW."initiative_id" AND "id" != NEW."id"
   4.953 +        FOR SHARE
   4.954 +      ) THEN
   4.955 +        "event_v" := 'new_draft_created';
   4.956 +      ELSE
   4.957 +        IF EXISTS (
   4.958 +          SELECT NULL FROM "initiative"
   4.959 +          WHERE "issue_id" = "initiative_row"."issue_id"
   4.960 +          AND "id" != "initiative_row"."id"
   4.961 +          FOR SHARE
   4.962 +        ) THEN
   4.963 +          "event_v" := 'initiative_created_in_existing_issue';
   4.964 +        ELSE
   4.965 +          "event_v" := 'initiative_created_in_new_issue';
   4.966 +        END IF;
   4.967 +      END IF;
   4.968 +      INSERT INTO "event" (
   4.969 +          "event", "member_id",
   4.970 +          "unit_id", "area_id", "issue_id", "state",
   4.971 +          "initiative_id", "draft_id"
   4.972 +        ) VALUES (
   4.973 +          "event_v", NEW."author_id",
   4.974 +          "area_row"."unit_id", "issue_row"."area_id",
   4.975 +          "initiative_row"."issue_id", "issue_row"."state",
   4.976 +          NEW."initiative_id", NEW."id"
   4.977 +        );
   4.978 +      RETURN NULL;
   4.979 +    END;
   4.980 +  $$;
   4.981 +
   4.982 +
   4.983 +CREATE OR REPLACE FUNCTION "write_event_initiative_revoked_trigger"()
   4.984 +  RETURNS TRIGGER
   4.985 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   4.986 +    DECLARE
   4.987 +      "issue_row"  "issue"%ROWTYPE;
   4.988 +      "area_row"   "area"%ROWTYPE;
   4.989 +      "draft_id_v" "draft"."id"%TYPE;
   4.990 +    BEGIN
   4.991 +      IF OLD."revoked" ISNULL AND NEW."revoked" NOTNULL THEN
   4.992 +        SELECT * INTO "issue_row" FROM "issue"
   4.993 +          WHERE "id" = NEW."issue_id" FOR SHARE;
   4.994 +        SELECT * INTO "area_row" FROM "area"
   4.995 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
   4.996 +        SELECT "id" INTO "draft_id_v" FROM "current_draft"
   4.997 +          WHERE "initiative_id" = NEW."id" FOR SHARE;
   4.998 +        INSERT INTO "event" (
   4.999 +            "event", "member_id",
  4.1000 +            "unit_id", "area_id", "issue_id", "state",
  4.1001 +            "initiative_id", "draft_id"
  4.1002 +          ) VALUES (
  4.1003 +            'initiative_revoked', NEW."revoked_by_member_id",
  4.1004 +            "area_row"."unit_id", "issue_row"."area_id",
  4.1005 +            NEW."issue_id", "issue_row"."state",
  4.1006 +            NEW."id", "draft_id_v"
  4.1007 +          );
  4.1008 +      END IF;
  4.1009 +      RETURN NULL;
  4.1010 +    END;
  4.1011 +  $$;
  4.1012 +
  4.1013 +
  4.1014 +CREATE OR REPLACE FUNCTION "write_event_suggestion_created_trigger"()
  4.1015 +  RETURNS TRIGGER
  4.1016 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1017 +    DECLARE
  4.1018 +      "initiative_row" "initiative"%ROWTYPE;
  4.1019 +      "issue_row"      "issue"%ROWTYPE;
  4.1020 +      "area_row"       "area"%ROWTYPE;
  4.1021 +    BEGIN
  4.1022 +      SELECT * INTO "initiative_row" FROM "initiative"
  4.1023 +        WHERE "id" = NEW."initiative_id" FOR SHARE;
  4.1024 +      SELECT * INTO "issue_row" FROM "issue"
  4.1025 +        WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  4.1026 +      SELECT * INTO "area_row" FROM "area"
  4.1027 +        WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1028 +      INSERT INTO "event" (
  4.1029 +          "event", "member_id",
  4.1030 +          "unit_id", "area_id", "issue_id", "state",
  4.1031 +          "initiative_id", "suggestion_id"
  4.1032 +        ) VALUES (
  4.1033 +          'suggestion_created', NEW."author_id",
  4.1034 +          "area_row"."unit_id", "issue_row"."area_id",
  4.1035 +          "initiative_row"."issue_id", "issue_row"."state",
  4.1036 +          NEW."initiative_id", NEW."id"
  4.1037 +        );
  4.1038 +      RETURN NULL;
  4.1039 +    END;
  4.1040 +  $$;
  4.1041 +
  4.1042 + 
  4.1043 +CREATE FUNCTION "write_event_suggestion_removed_trigger"()
  4.1044 +  RETURNS TRIGGER
  4.1045 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1046 +    DECLARE
  4.1047 +      "initiative_row" "initiative"%ROWTYPE;
  4.1048 +      "issue_row"      "issue"%ROWTYPE;
  4.1049 +      "area_row"       "area"%ROWTYPE;
  4.1050 +    BEGIN
  4.1051 +      SELECT * INTO "initiative_row" FROM "initiative"
  4.1052 +        WHERE "id" = OLD."initiative_id" FOR SHARE;
  4.1053 +      IF "initiative_row"."id" NOTNULL THEN
  4.1054 +        SELECT * INTO "issue_row" FROM "issue"
  4.1055 +          WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  4.1056 +        SELECT * INTO "area_row" FROM "area"
  4.1057 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1058 +        INSERT INTO "event" (
  4.1059 +            "event",
  4.1060 +            "unit_id", "area_id", "issue_id", "state",
  4.1061 +            "initiative_id", "suggestion_id"
  4.1062 +          ) VALUES (
  4.1063 +            'suggestion_removed',
  4.1064 +            "area_row"."unit_id", "issue_row"."area_id",
  4.1065 +            "initiative_row"."issue_id", "issue_row"."state",
  4.1066 +            OLD."initiative_id", OLD."id"
  4.1067 +          );
  4.1068 +      END IF;
  4.1069 +      RETURN NULL;
  4.1070 +    END;
  4.1071 +  $$;
  4.1072 +
  4.1073 +CREATE TRIGGER "write_event_suggestion_removed"
  4.1074 +  AFTER DELETE ON "suggestion" FOR EACH ROW EXECUTE PROCEDURE
  4.1075 +  "write_event_suggestion_removed_trigger"();
  4.1076 +
  4.1077 +COMMENT ON FUNCTION "write_event_suggestion_removed_trigger"()      IS 'Implementation of trigger "write_event_suggestion_removed" on table "issue"';
  4.1078 +COMMENT ON TRIGGER "write_event_suggestion_removed" ON "suggestion" IS 'Create entry in "event" table on suggestion creation';
  4.1079 +
  4.1080 +
  4.1081 +CREATE FUNCTION "write_event_member_trigger"()
  4.1082 +  RETURNS TRIGGER
  4.1083 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1084 +    BEGIN
  4.1085 +      IF TG_OP = 'INSERT' THEN
  4.1086 +        IF NEW."activated" NOTNULL THEN
  4.1087 +          INSERT INTO "event" ("event", "member_id")
  4.1088 +            VALUES ('member_activated', NEW."id");
  4.1089 +        END IF;
  4.1090 +        IF NEW."active" THEN
  4.1091 +          INSERT INTO "event" ("event", "member_id", "boolean_value")
  4.1092 +            VALUES ('member_active', NEW."id", TRUE);
  4.1093 +        END IF;
  4.1094 +      ELSIF TG_OP = 'UPDATE' THEN
  4.1095 +        IF OLD."id" != NEW."id" THEN
  4.1096 +          RAISE EXCEPTION 'Cannot change member ID';
  4.1097 +        END IF;
  4.1098 +        IF OLD."name" != NEW."name" THEN
  4.1099 +          INSERT INTO "event" (
  4.1100 +            "event", "member_id", "text_value", "old_text_value"
  4.1101 +          ) VALUES (
  4.1102 +            'member_name_updated', NEW."id", NEW."name", OLD."name"
  4.1103 +          );
  4.1104 +        END IF;
  4.1105 +        IF OLD."active" != NEW."active" THEN
  4.1106 +          INSERT INTO "event" ("event", "member_id", "boolean_value") VALUES (
  4.1107 +            'member_active', NEW."id", NEW."active"
  4.1108 +          );
  4.1109 +        END IF;
  4.1110 +        IF
  4.1111 +          OLD."activated" NOTNULL AND
  4.1112 +          NEW."last_login"      ISNULL AND
  4.1113 +          NEW."login"           ISNULL AND
  4.1114 +          NEW."authority_login" ISNULL AND
  4.1115 +          NEW."locked"          = TRUE
  4.1116 +        THEN
  4.1117 +          INSERT INTO "event" ("event", "member_id")
  4.1118 +            VALUES ('member_removed', NEW."id");
  4.1119 +        END IF;
  4.1120 +      END IF;
  4.1121 +      RETURN NULL;
  4.1122 +    END;
  4.1123 +  $$;
  4.1124 +
  4.1125 +CREATE TRIGGER "write_event_member"
  4.1126 +  AFTER INSERT OR UPDATE ON "member" FOR EACH ROW EXECUTE PROCEDURE
  4.1127 +  "write_event_member_trigger"();
  4.1128 +
  4.1129 +COMMENT ON FUNCTION "write_event_member_trigger"()  IS 'Implementation of trigger "write_event_member" on table "member"';
  4.1130 +COMMENT ON TRIGGER "write_event_member" ON "member" IS 'Create entries in "event" table on insertion to member table';
  4.1131 +
  4.1132 +
  4.1133 +CREATE FUNCTION "write_event_member_profile_updated_trigger"()
  4.1134 +  RETURNS TRIGGER
  4.1135 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1136 +    BEGIN
  4.1137 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  4.1138 +        IF EXISTS (SELECT NULL FROM "member" WHERE "id" = OLD."member_id") THEN
  4.1139 +          INSERT INTO "event" ("event", "member_id") VALUES (
  4.1140 +            'member_profile_updated', OLD."member_id"
  4.1141 +          );
  4.1142 +        END IF;
  4.1143 +      END IF;
  4.1144 +      IF TG_OP = 'UPDATE' THEN
  4.1145 +        IF OLD."member_id" = NEW."member_id" THEN
  4.1146 +          RETURN NULL;
  4.1147 +        END IF;
  4.1148 +      END IF;
  4.1149 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  4.1150 +        INSERT INTO "event" ("event", "member_id") VALUES (
  4.1151 +          'member_profile_updated', NEW."member_id"
  4.1152 +        );
  4.1153 +      END IF;
  4.1154 +      RETURN NULL;
  4.1155 +    END;
  4.1156 +  $$;
  4.1157 +
  4.1158 +CREATE TRIGGER "write_event_member_profile_updated"
  4.1159 +  AFTER INSERT OR UPDATE OR DELETE ON "member_profile"
  4.1160 +  FOR EACH ROW EXECUTE PROCEDURE
  4.1161 +  "write_event_member_profile_updated_trigger"();
  4.1162 +
  4.1163 +COMMENT ON FUNCTION "write_event_member_profile_updated_trigger"()          IS 'Implementation of trigger "write_event_member_profile_updated" on table "member_profile"';
  4.1164 +COMMENT ON TRIGGER "write_event_member_profile_updated" ON "member_profile" IS 'Creates entries in "event" table on member profile update';
  4.1165 +
  4.1166 +
  4.1167 +CREATE FUNCTION "write_event_member_image_updated_trigger"()
  4.1168 +  RETURNS TRIGGER
  4.1169 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1170 +    BEGIN
  4.1171 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  4.1172 +        IF NOT OLD."scaled" THEN
  4.1173 +          IF EXISTS (SELECT NULL FROM "member" WHERE "id" = OLD."member_id") THEN
  4.1174 +            INSERT INTO "event" ("event", "member_id") VALUES (
  4.1175 +              'member_image_updated', OLD."member_id"
  4.1176 +            );
  4.1177 +          END IF;
  4.1178 +        END IF;
  4.1179 +      END IF;
  4.1180 +      IF TG_OP = 'UPDATE' THEN
  4.1181 +        IF
  4.1182 +          OLD."member_id" = NEW."member_id" AND
  4.1183 +          OLD."scaled" = NEW."scaled"
  4.1184 +        THEN
  4.1185 +          RETURN NULL;
  4.1186 +        END IF;
  4.1187 +      END IF;
  4.1188 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  4.1189 +        IF NOT NEW."scaled" THEN
  4.1190 +          INSERT INTO "event" ("event", "member_id") VALUES (
  4.1191 +            'member_image_updated', NEW."member_id"
  4.1192 +          );
  4.1193 +        END IF;
  4.1194 +      END IF;
  4.1195 +      RETURN NULL;
  4.1196 +    END;
  4.1197 +  $$;
  4.1198 +
  4.1199 +CREATE TRIGGER "write_event_member_image_updated"
  4.1200 +  AFTER INSERT OR UPDATE OR DELETE ON "member_image"
  4.1201 +  FOR EACH ROW EXECUTE PROCEDURE
  4.1202 +  "write_event_member_image_updated_trigger"();
  4.1203 +
  4.1204 +COMMENT ON FUNCTION "write_event_member_image_updated_trigger"()        IS 'Implementation of trigger "write_event_member_image_updated" on table "member_image"';
  4.1205 +COMMENT ON TRIGGER "write_event_member_image_updated" ON "member_image" IS 'Creates entries in "event" table on member image update';
  4.1206 +
  4.1207 +
  4.1208 +CREATE FUNCTION "write_event_interest_trigger"()
  4.1209 +  RETURNS TRIGGER
  4.1210 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1211 +    DECLARE
  4.1212 +      "issue_row" "issue"%ROWTYPE;
  4.1213 +      "area_row"  "area"%ROWTYPE;
  4.1214 +    BEGIN
  4.1215 +      IF TG_OP = 'UPDATE' THEN
  4.1216 +        IF OLD = NEW THEN
  4.1217 +          RETURN NULL;
  4.1218 +        END IF;
  4.1219 +      END IF;
  4.1220 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  4.1221 +        SELECT * INTO "issue_row" FROM "issue"
  4.1222 +          WHERE "id" = OLD."issue_id" FOR SHARE;
  4.1223 +        SELECT * INTO "area_row" FROM "area"
  4.1224 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1225 +        IF "issue_row"."id" NOTNULL THEN
  4.1226 +          INSERT INTO "event" (
  4.1227 +              "event", "member_id",
  4.1228 +              "unit_id", "area_id", "issue_id", "state",
  4.1229 +              "boolean_value"
  4.1230 +            ) VALUES (
  4.1231 +              'interest', OLD."member_id",
  4.1232 +              "area_row"."unit_id", "issue_row"."area_id",
  4.1233 +              OLD."issue_id", "issue_row"."state",
  4.1234 +              FALSE
  4.1235 +            );
  4.1236 +        END IF;
  4.1237 +      END IF;
  4.1238 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  4.1239 +        SELECT * INTO "issue_row" FROM "issue"
  4.1240 +          WHERE "id" = NEW."issue_id" FOR SHARE;
  4.1241 +        SELECT * INTO "area_row" FROM "area"
  4.1242 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1243 +        INSERT INTO "event" (
  4.1244 +            "event", "member_id",
  4.1245 +            "unit_id", "area_id", "issue_id", "state",
  4.1246 +            "boolean_value"
  4.1247 +          ) VALUES (
  4.1248 +            'interest', NEW."member_id",
  4.1249 +            "area_row"."unit_id", "issue_row"."area_id",
  4.1250 +            NEW."issue_id", "issue_row"."state",
  4.1251 +            TRUE
  4.1252 +          );
  4.1253 +      END IF;
  4.1254 +      RETURN NULL;
  4.1255 +    END;
  4.1256 +  $$;
  4.1257 +
  4.1258 +CREATE TRIGGER "write_event_interest"
  4.1259 +  AFTER INSERT OR UPDATE OR DELETE ON "interest" FOR EACH ROW EXECUTE PROCEDURE
  4.1260 +  "write_event_interest_trigger"();
  4.1261 +
  4.1262 +COMMENT ON FUNCTION "write_event_interest_trigger"()  IS 'Implementation of trigger "write_event_interest_inserted" on table "interest"';
  4.1263 +COMMENT ON TRIGGER "write_event_interest" ON "interest" IS 'Create entry in "event" table on adding or removing interest';
  4.1264 +
  4.1265 +
  4.1266 +CREATE FUNCTION "write_event_initiator_trigger"()
  4.1267 +  RETURNS TRIGGER
  4.1268 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1269 +    DECLARE
  4.1270 +      "initiative_row" "initiative"%ROWTYPE;
  4.1271 +      "issue_row"      "issue"%ROWTYPE;
  4.1272 +      "area_row"       "area"%ROWTYPE;
  4.1273 +    BEGIN
  4.1274 +      IF TG_OP = 'UPDATE' THEN
  4.1275 +        IF
  4.1276 +          OLD."initiative_id" = NEW."initiative_id" AND
  4.1277 +          OLD."member_id" = NEW."member_id" AND
  4.1278 +          coalesce(OLD."accepted", FALSE) = coalesce(NEW."accepted", FALSE)
  4.1279 +        THEN
  4.1280 +          RETURN NULL;
  4.1281 +        END IF;
  4.1282 +      END IF;
  4.1283 +      IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND NOT "accepted_v" THEN
  4.1284 +        IF coalesce(OLD."accepted", FALSE) = TRUE THEN
  4.1285 +          SELECT * INTO "initiative_row" FROM "initiative"
  4.1286 +            WHERE "id" = OLD."initiative_id" FOR SHARE;
  4.1287 +          IF "initiative_row"."id" NOTNULL THEN
  4.1288 +            SELECT * INTO "issue_row" FROM "issue"
  4.1289 +              WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  4.1290 +            SELECT * INTO "area_row" FROM "area"
  4.1291 +              WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1292 +            INSERT INTO "event" (
  4.1293 +                "event", "member_id",
  4.1294 +                "unit_id", "area_id", "issue_id", "state",
  4.1295 +                "initiative_id", "boolean_value"
  4.1296 +              ) VALUES (
  4.1297 +                'initiator', OLD."member_id",
  4.1298 +                "area_row"."unit_id", "issue_row"."area_id",
  4.1299 +                "issue_row"."id", "issue_row"."state",
  4.1300 +                OLD."initiative_id", FALSE
  4.1301 +              );
  4.1302 +          END IF;
  4.1303 +        END IF;
  4.1304 +      END IF;
  4.1305 +      IF TG_OP = 'UPDATE' AND NOT "rejected_v" THEN
  4.1306 +        IF coalesce(NEW."accepted", FALSE) = TRUE THEN
  4.1307 +          SELECT * INTO "initiative_row" FROM "initiative"
  4.1308 +            WHERE "id" = NEW."initiative_id" FOR SHARE;
  4.1309 +          SELECT * INTO "issue_row" FROM "issue"
  4.1310 +            WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  4.1311 +          SELECT * INTO "area_row" FROM "area"
  4.1312 +            WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1313 +          INSERT INTO "event" (
  4.1314 +              "event", "member_id",
  4.1315 +              "unit_id", "area_id", "issue_id", "state",
  4.1316 +              "initiative_id", "boolean_value"
  4.1317 +            ) VALUES (
  4.1318 +              'initiator', NEW."member_id",
  4.1319 +              "area_row"."unit_id", "issue_row"."area_id",
  4.1320 +              "issue_row"."id", "issue_row"."state",
  4.1321 +              NEW."initiative_id", TRUE
  4.1322 +            );
  4.1323 +        END IF;
  4.1324 +      END IF;
  4.1325 +      RETURN NULL;
  4.1326 +    END;
  4.1327 +  $$;
  4.1328 +
  4.1329 +CREATE TRIGGER "write_event_initiator"
  4.1330 +  AFTER UPDATE OR DELETE ON "initiator" FOR EACH ROW EXECUTE PROCEDURE
  4.1331 +  "write_event_initiator_trigger"();
  4.1332 +
  4.1333 +COMMENT ON FUNCTION "write_event_initiator_trigger"()     IS 'Implementation of trigger "write_event_initiator" on table "initiator"';
  4.1334 +COMMENT ON TRIGGER "write_event_initiator" ON "initiator" IS 'Create entry in "event" table when accepting or removing initiatorship (NOTE: trigger does not fire on INSERT to avoid events on initiative creation)';
  4.1335 +
  4.1336 +
  4.1337 +CREATE FUNCTION "write_event_support_trigger"()
  4.1338 +  RETURNS TRIGGER
  4.1339 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1340 +    DECLARE
  4.1341 +      "issue_row" "issue"%ROWTYPE;
  4.1342 +      "area_row"  "area"%ROWTYPE;
  4.1343 +    BEGIN
  4.1344 +      IF TG_OP = 'UPDATE' THEN
  4.1345 +        IF
  4.1346 +          OLD."initiative_id" = NEW."initiative_id" AND
  4.1347 +          OLD."member_id" = NEW."member_id"
  4.1348 +        THEN
  4.1349 +          IF OLD."draft_id" != NEW."draft_id" THEN
  4.1350 +            SELECT * INTO "issue_row" FROM "issue"
  4.1351 +              WHERE "id" = NEW."issue_id" FOR SHARE;
  4.1352 +            SELECT * INTO "area_row" FROM "area"
  4.1353 +              WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1354 +            INSERT INTO "event" (
  4.1355 +                "event", "member_id",
  4.1356 +                "unit_id", "area_id", "issue_id", "state",
  4.1357 +                "initiative_id", "draft_id"
  4.1358 +              ) VALUES (
  4.1359 +                'support_updated', NEW."member_id",
  4.1360 +                "area_row"."unit_id", "issue_row"."area_id",
  4.1361 +                "issue_row"."id", "issue_row"."state",
  4.1362 +                NEW."initiative_id", NEW."draft_id"
  4.1363 +              );
  4.1364 +          END IF;
  4.1365 +          RETURN NULL;
  4.1366 +        END IF;
  4.1367 +      END IF;
  4.1368 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  4.1369 +        IF EXISTS (
  4.1370 +          SELECT NULL FROM "initiative" WHERE "id" = OLD."initiative_id"
  4.1371 +          FOR SHARE
  4.1372 +        ) THEN
  4.1373 +          SELECT * INTO "issue_row" FROM "issue"
  4.1374 +            WHERE "id" = OLD."issue_id" FOR SHARE;
  4.1375 +          SELECT * INTO "area_row" FROM "area"
  4.1376 +            WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1377 +          INSERT INTO "event" (
  4.1378 +              "event", "member_id",
  4.1379 +              "unit_id", "area_id", "issue_id", "state",
  4.1380 +              "initiative_id", "draft_id", "boolean_value"
  4.1381 +            ) VALUES (
  4.1382 +              'support', OLD."member_id",
  4.1383 +              "area_row"."unit_id", "issue_row"."area_id",
  4.1384 +              "issue_row"."id", "issue_row"."state",
  4.1385 +              OLD."initiative_id", OLD."draft_id", FALSE
  4.1386 +            );
  4.1387 +        END IF;
  4.1388 +      END IF;
  4.1389 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  4.1390 +        SELECT * INTO "issue_row" FROM "issue"
  4.1391 +          WHERE "id" = NEW."issue_id" FOR SHARE;
  4.1392 +        SELECT * INTO "area_row" FROM "area"
  4.1393 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1394 +        INSERT INTO "event" (
  4.1395 +            "event", "member_id",
  4.1396 +            "unit_id", "area_id", "issue_id", "state",
  4.1397 +            "initiative_id", "draft_id", "boolean_value"
  4.1398 +          ) VALUES (
  4.1399 +            'support', NEW."member_id",
  4.1400 +            "area_row"."unit_id", "issue_row"."area_id",
  4.1401 +            "issue_row"."id", "issue_row"."state",
  4.1402 +            NEW."initiative_id", NEW."draft_id", TRUE
  4.1403 +          );
  4.1404 +      END IF;
  4.1405 +      RETURN NULL;
  4.1406 +    END;
  4.1407 +  $$;
  4.1408 +
  4.1409 +CREATE TRIGGER "write_event_support"
  4.1410 +  AFTER INSERT OR UPDATE OR DELETE ON "supporter" FOR EACH ROW EXECUTE PROCEDURE
  4.1411 +  "write_event_support_trigger"();
  4.1412 +
  4.1413 +COMMENT ON FUNCTION "write_event_support_trigger"()     IS 'Implementation of trigger "write_event_support" on table "supporter"';
  4.1414 +COMMENT ON TRIGGER "write_event_support" ON "supporter" IS 'Create entry in "event" table when adding, updating, or removing support';
  4.1415 +
  4.1416 +
  4.1417 +CREATE FUNCTION "write_event_suggestion_rated_trigger"()
  4.1418 +  RETURNS TRIGGER
  4.1419 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1420 +    DECLARE
  4.1421 +      "same_pkey_v"    BOOLEAN = FALSE;
  4.1422 +      "initiative_row" "initiative"%ROWTYPE;
  4.1423 +      "issue_row"      "issue"%ROWTYPE;
  4.1424 +      "area_row"       "area"%ROWTYPE;
  4.1425 +    BEGIN
  4.1426 +      IF TG_OP = 'UPDATE' THEN
  4.1427 +        IF
  4.1428 +          OLD."suggestion_id" = NEW."suggestion_id" AND
  4.1429 +          OLD."member_id"     = NEW."member_id"
  4.1430 +        THEN
  4.1431 +          IF
  4.1432 +            OLD."degree"    = NEW."degree" AND
  4.1433 +            OLD."fulfilled" = NEW."fulfilled"
  4.1434 +          THEN
  4.1435 +            RETURN NULL;
  4.1436 +          END IF;
  4.1437 +          "same_pkey_v" := TRUE;
  4.1438 +        END IF;
  4.1439 +      END IF;
  4.1440 +      IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') AND NOT "same_pkey_v" THEN
  4.1441 +        IF EXISTS (
  4.1442 +          SELECT NULL FROM "suggestion" WHERE "id" = OLD."suggestion_id"
  4.1443 +          FOR SHARE
  4.1444 +        ) THEN
  4.1445 +          SELECT * INTO "initiative_row" FROM "initiative"
  4.1446 +            WHERE "id" = OLD."initiative_id" FOR SHARE;
  4.1447 +          SELECT * INTO "issue_row" FROM "issue"
  4.1448 +            WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  4.1449 +          SELECT * INTO "area_row" FROM "area"
  4.1450 +            WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1451 +          INSERT INTO "event" (
  4.1452 +              "event", "member_id",
  4.1453 +              "unit_id", "area_id", "issue_id", "state",
  4.1454 +              "initiative_id", "suggestion_id",
  4.1455 +              "boolean_value", "numeric_value"
  4.1456 +            ) VALUES (
  4.1457 +              'suggestion_rated', OLD."member_id",
  4.1458 +              "area_row"."unit_id", "issue_row"."area_id",
  4.1459 +              "initiative_row"."issue_id", "issue_row"."state",
  4.1460 +              OLD."initiative_id", OLD."suggestion_id",
  4.1461 +              NULL, 0
  4.1462 +            );
  4.1463 +        END IF;
  4.1464 +      END IF;
  4.1465 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  4.1466 +        SELECT * INTO "initiative_row" FROM "initiative"
  4.1467 +          WHERE "id" = NEW."initiative_id" FOR SHARE;
  4.1468 +        SELECT * INTO "issue_row" FROM "issue"
  4.1469 +          WHERE "id" = "initiative_row"."issue_id" FOR SHARE;
  4.1470 +        SELECT * INTO "area_row" FROM "area"
  4.1471 +          WHERE "id" = "issue_row"."area_id" FOR SHARE;
  4.1472 +        INSERT INTO "event" (
  4.1473 +            "event", "member_id",
  4.1474 +            "unit_id", "area_id", "issue_id", "state",
  4.1475 +            "initiative_id", "suggestion_id",
  4.1476 +            "boolean_value", "numeric_value"
  4.1477 +          ) VALUES (
  4.1478 +            'suggestion_rated', NEW."member_id",
  4.1479 +            "area_row"."unit_id", "issue_row"."area_id",
  4.1480 +            "initiative_row"."issue_id", "issue_row"."state",
  4.1481 +            NEW."initiative_id", NEW."suggestion_id",
  4.1482 +            NEW."fulfilled", NEW."degree"
  4.1483 +          );
  4.1484 +      END IF;
  4.1485 +      RETURN NULL;
  4.1486 +    END;
  4.1487 +  $$;
  4.1488 +
  4.1489 +CREATE TRIGGER "write_event_suggestion_rated"
  4.1490 +  AFTER INSERT OR UPDATE OR DELETE ON "opinion" FOR EACH ROW EXECUTE PROCEDURE
  4.1491 +  "write_event_suggestion_rated_trigger"();
  4.1492 +
  4.1493 +COMMENT ON FUNCTION "write_event_suggestion_rated_trigger"()   IS 'Implementation of trigger "write_event_suggestion_rated" on table "opinion"';
  4.1494 +COMMENT ON TRIGGER "write_event_suggestion_rated" ON "opinion" IS 'Create entry in "event" table when adding, updating, or removing support';
  4.1495 +
  4.1496 +
  4.1497 +CREATE FUNCTION "write_event_delegation_trigger"()
  4.1498 +  RETURNS TRIGGER
  4.1499 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1500 +    DECLARE
  4.1501 +      "issue_row" "issue"%ROWTYPE;
  4.1502 +      "area_row"  "area"%ROWTYPE;
  4.1503 +    BEGIN
  4.1504 +      IF TG_OP = 'DELETE' THEN
  4.1505 +        IF EXISTS (
  4.1506 +          SELECT NULL FROM "member" WHERE "id" = OLD."truster_id"
  4.1507 +        ) AND (CASE OLD."scope"
  4.1508 +          WHEN 'unit'::"delegation_scope" THEN EXISTS (
  4.1509 +            SELECT NULL FROM "unit" WHERE "id" = OLD."unit_id"
  4.1510 +          )
  4.1511 +          WHEN 'area'::"delegation_scope" THEN EXISTS (
  4.1512 +            SELECT NULL FROM "area" WHERE "id" = OLD."area_id"
  4.1513 +          )
  4.1514 +          WHEN 'issue'::"delegation_scope" THEN EXISTS (
  4.1515 +            SELECT NULL FROM "issue" WHERE "id" = OLD."issue_id"
  4.1516 +          )
  4.1517 +        END) THEN
  4.1518 +          SELECT * INTO "issue_row" FROM "issue"
  4.1519 +            WHERE "id" = OLD."issue_id" FOR SHARE;
  4.1520 +          SELECT * INTO "area_row" FROM "area"
  4.1521 +            WHERE "id" = COALESCE(OLD."area_id", "issue_row"."area_id")
  4.1522 +            FOR SHARE;
  4.1523 +          INSERT INTO "event" (
  4.1524 +              "event", "member_id", "scope",
  4.1525 +              "unit_id", "area_id", "issue_id", "state",
  4.1526 +              "boolean_value"
  4.1527 +            ) VALUES (
  4.1528 +              'delegation', OLD."truster_id", OLD."scope",
  4.1529 +              COALESCE(OLD."unit_id", "area_row"."unit_id"), "area_row"."id",
  4.1530 +              OLD."issue_id", "issue_row"."state",
  4.1531 +              FALSE
  4.1532 +            );
  4.1533 +        END IF;
  4.1534 +      ELSE
  4.1535 +        SELECT * INTO "issue_row" FROM "issue"
  4.1536 +          WHERE "id" = NEW."issue_id" FOR SHARE;
  4.1537 +        SELECT * INTO "area_row" FROM "area"
  4.1538 +          WHERE "id" = COALESCE(NEW."area_id", "issue_row"."area_id")
  4.1539 +          FOR SHARE;
  4.1540 +        INSERT INTO "event" (
  4.1541 +            "event", "member_id", "other_member_id", "scope",
  4.1542 +            "unit_id", "area_id", "issue_id", "state",
  4.1543 +            "boolean_value"
  4.1544 +          ) VALUES (
  4.1545 +            'delegation', NEW."truster_id", NEW."trustee_id", NEW."scope",
  4.1546 +            COALESCE(NEW."unit_id", "area_row"."unit_id"), "area_row"."id",
  4.1547 +            NEW."issue_id", "issue_row"."state",
  4.1548 +            TRUE
  4.1549 +          );
  4.1550 +      END IF;
  4.1551 +      RETURN NULL;
  4.1552 +    END;
  4.1553 +  $$;
  4.1554 +
  4.1555 +CREATE TRIGGER "write_event_delegation"
  4.1556 +  AFTER INSERT OR UPDATE OR DELETE ON "delegation" FOR EACH ROW EXECUTE PROCEDURE
  4.1557 +  "write_event_delegation_trigger"();
  4.1558 +
  4.1559 +COMMENT ON FUNCTION "write_event_delegation_trigger"()      IS 'Implementation of trigger "write_event_delegation" on table "delegation"';
  4.1560 +COMMENT ON TRIGGER "write_event_delegation" ON "delegation" IS 'Create entry in "event" table when adding, updating, or removing a delegation';
  4.1561 +
  4.1562 +
  4.1563 +CREATE FUNCTION "write_event_contact_trigger"()
  4.1564 +  RETURNS TRIGGER
  4.1565 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1566 +    BEGIN
  4.1567 +      IF TG_OP = 'UPDATE' THEN
  4.1568 +        IF
  4.1569 +          OLD."member_id"       = NEW."member_id" AND
  4.1570 +          OLD."other_member_id" = NEW."other_member_id" AND
  4.1571 +          OLD."public"          = NEW."public"
  4.1572 +        THEN
  4.1573 +          RETURN NULL;
  4.1574 +        END IF;
  4.1575 +      END IF;
  4.1576 +      IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
  4.1577 +        IF OLD."public" THEN
  4.1578 +          IF EXISTS (
  4.1579 +            SELECT NULL FROM "member" WHERE "id" = OLD."member_id"
  4.1580 +            FOR SHARE
  4.1581 +          ) AND EXISTS (
  4.1582 +            SELECT NULL FROM "member" WHERE "id" = OLD."other_member_id"
  4.1583 +            FOR SHARE
  4.1584 +          ) THEN
  4.1585 +            INSERT INTO "event" (
  4.1586 +                "event", "member_id", "other_member_id", "boolean_value"
  4.1587 +              ) VALUES (
  4.1588 +                'contact', OLD."member_id", OLD."other_member_id", FALSE
  4.1589 +              );
  4.1590 +          END IF;
  4.1591 +        END IF;
  4.1592 +      END IF;
  4.1593 +      IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
  4.1594 +        IF NEW."public" THEN
  4.1595 +          INSERT INTO "event" (
  4.1596 +              "event", "member_id", "other_member_id", "boolean_value"
  4.1597 +            ) VALUES (
  4.1598 +              'contact', NEW."member_id", NEW."other_member_id", TRUE
  4.1599 +            );
  4.1600 +        END IF;
  4.1601 +      END IF;
  4.1602 +      RETURN NULL;
  4.1603 +    END;
  4.1604 +  $$;
  4.1605 +
  4.1606 +CREATE TRIGGER "write_event_contact"
  4.1607 +  AFTER INSERT OR UPDATE OR DELETE ON "contact" FOR EACH ROW EXECUTE PROCEDURE
  4.1608 +  "write_event_contact_trigger"();
  4.1609 +
  4.1610 +COMMENT ON FUNCTION "write_event_contact_trigger"()   IS 'Implementation of trigger "write_event_contact" on table "contact"';
  4.1611 +COMMENT ON TRIGGER "write_event_contact" ON "contact" IS 'Create entry in "event" table when adding or removing public contacts';
  4.1612 +
  4.1613 +
  4.1614 +CREATE FUNCTION "send_event_notify_trigger"()
  4.1615 +  RETURNS TRIGGER
  4.1616 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1617 +    BEGIN
  4.1618 +      EXECUTE 'NOTIFY "event", ''' || NEW."event" || '''';
  4.1619 +      RETURN NULL;
  4.1620 +    END;
  4.1621 +  $$;
  4.1622 +
  4.1623 +CREATE TRIGGER "send_notify"
  4.1624 +  AFTER INSERT OR UPDATE ON "event" FOR EACH ROW EXECUTE PROCEDURE
  4.1625 +  "send_event_notify_trigger"();
  4.1626 +
  4.1627 +
  4.1628 +CREATE FUNCTION "delete_extended_scope_tokens_trigger"()
  4.1629 +  RETURNS TRIGGER
  4.1630 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1631 +    DECLARE
  4.1632 +      "system_application_row" "system_application"%ROWTYPE;
  4.1633 +    BEGIN
  4.1634 +      IF OLD."system_application_id" NOTNULL THEN
  4.1635 +        SELECT * FROM "system_application" INTO "system_application_row"
  4.1636 +          WHERE "id" = OLD."system_application_id";
  4.1637 +        DELETE FROM "token"
  4.1638 +          WHERE "member_id" = OLD."member_id"
  4.1639 +          AND "system_application_id" = OLD."system_application_id"
  4.1640 +          AND NOT COALESCE(
  4.1641 +            regexp_split_to_array("scope", E'\\s+') <@
  4.1642 +            regexp_split_to_array(
  4.1643 +              "system_application_row"."automatic_scope", E'\\s+'
  4.1644 +            ),
  4.1645 +            FALSE
  4.1646 +          );
  4.1647 +      END IF;
  4.1648 +      RETURN OLD;
  4.1649 +    END;
  4.1650 +  $$;
  4.1651 +
  4.1652 +CREATE TRIGGER "delete_extended_scope_tokens"
  4.1653 +  BEFORE DELETE ON "member_application" FOR EACH ROW EXECUTE PROCEDURE
  4.1654 +  "delete_extended_scope_tokens_trigger"();
  4.1655 +
  4.1656 +
  4.1657 +CREATE FUNCTION "detach_token_from_session_trigger"()
  4.1658 +  RETURNS TRIGGER
  4.1659 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1660 +    BEGIN
  4.1661 +      UPDATE "token" SET "session_id" = NULL
  4.1662 +        WHERE "session_id" = OLD."id";
  4.1663 +      RETURN OLD;
  4.1664 +    END;
  4.1665 +  $$;
  4.1666 +
  4.1667 +CREATE TRIGGER "detach_token_from_session"
  4.1668 +  BEFORE DELETE ON "session" FOR EACH ROW EXECUTE PROCEDURE
  4.1669 +  "detach_token_from_session_trigger"();
  4.1670 +
  4.1671 +
  4.1672 +CREATE FUNCTION "delete_non_detached_scope_with_session_trigger"()
  4.1673 +  RETURNS TRIGGER
  4.1674 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1675 +    BEGIN
  4.1676 +      IF NEW."session_id" ISNULL THEN
  4.1677 +        SELECT coalesce(string_agg("element", ' '), '') INTO NEW."scope"
  4.1678 +          FROM unnest(regexp_split_to_array(NEW."scope", E'\\s+')) AS "element"
  4.1679 +          WHERE "element" LIKE '%_detached';
  4.1680 +      END IF;
  4.1681 +      RETURN NEW;
  4.1682 +    END;
  4.1683 +  $$;
  4.1684 +
  4.1685 +CREATE TRIGGER "delete_non_detached_scope_with_session"
  4.1686 +  BEFORE INSERT OR UPDATE ON "token" FOR EACH ROW EXECUTE PROCEDURE
  4.1687 +  "delete_non_detached_scope_with_session_trigger"();
  4.1688 +
  4.1689 +
  4.1690 +CREATE FUNCTION "delete_token_with_empty_scope_trigger"()
  4.1691 +  RETURNS TRIGGER
  4.1692 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1693 +    BEGIN
  4.1694 +      IF NEW."scope" = '' THEN
  4.1695 +        DELETE FROM "token" WHERE "id" = NEW."id";
  4.1696 +      END IF;
  4.1697 +      RETURN NULL;
  4.1698 +    END;
  4.1699 +  $$;
  4.1700 +
  4.1701 +CREATE TRIGGER "delete_token_with_empty_scope"
  4.1702 +  AFTER INSERT OR UPDATE ON "token" FOR EACH ROW EXECUTE PROCEDURE
  4.1703 +  "delete_token_with_empty_scope_trigger"();
  4.1704 +
  4.1705 +
  4.1706 +CREATE FUNCTION "delete_snapshot_on_partial_delete_trigger"()
  4.1707 +  RETURNS TRIGGER
  4.1708 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1709 +    BEGIN
  4.1710 +      IF TG_OP = 'UPDATE' THEN
  4.1711 +        IF
  4.1712 +          OLD."snapshot_id" = NEW."snapshot_id" AND
  4.1713 +          OLD."issue_id" = NEW."issue_id"
  4.1714 +        THEN
  4.1715 +          RETURN NULL;
  4.1716 +        END IF;
  4.1717 +      END IF;
  4.1718 +      DELETE FROM "snapshot" WHERE "id" = OLD."snapshot_id";
  4.1719 +      RETURN NULL;
  4.1720 +    END;
  4.1721 +  $$;
  4.1722 +
  4.1723 +CREATE TRIGGER "delete_snapshot_on_partial_delete"
  4.1724 +  AFTER UPDATE OR DELETE ON "snapshot_issue"
  4.1725 +  FOR EACH ROW EXECUTE PROCEDURE
  4.1726 +  "delete_snapshot_on_partial_delete_trigger"();
  4.1727 +
  4.1728 +COMMENT ON FUNCTION "delete_snapshot_on_partial_delete_trigger"()          IS 'Implementation of trigger "delete_snapshot_on_partial_delete" on table "snapshot_issue"';
  4.1729 +COMMENT ON TRIGGER "delete_snapshot_on_partial_delete" ON "snapshot_issue" IS 'Deletes whole snapshot if one issue is deleted from the snapshot';
  4.1730 +
  4.1731 +
  4.1732 +CREATE FUNCTION "copy_current_draft_data"
  4.1733 +  ("initiative_id_p" "initiative"."id"%TYPE )
  4.1734 +  RETURNS VOID
  4.1735 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1736 +    BEGIN
  4.1737 +      PERFORM NULL FROM "initiative" WHERE "id" = "initiative_id_p"
  4.1738 +        FOR UPDATE;
  4.1739 +      UPDATE "initiative" SET
  4.1740 +        "location" = "draft"."location",
  4.1741 +        "draft_text_search_data" = "draft"."text_search_data"
  4.1742 +        FROM "current_draft" AS "draft"
  4.1743 +        WHERE "initiative"."id" = "initiative_id_p"
  4.1744 +        AND "draft"."initiative_id" = "initiative_id_p";
  4.1745 +    END;
  4.1746 +  $$;
  4.1747 +
  4.1748 +COMMENT ON FUNCTION "copy_current_draft_data"
  4.1749 +  ( "initiative"."id"%TYPE )
  4.1750 +  IS 'Helper function for function "copy_current_draft_data_trigger"';
  4.1751 +
  4.1752 +
  4.1753 +CREATE FUNCTION "copy_current_draft_data_trigger"()
  4.1754 +  RETURNS TRIGGER
  4.1755 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.1756 +    BEGIN
  4.1757 +      IF TG_OP='DELETE' THEN
  4.1758 +        PERFORM "copy_current_draft_data"(OLD."initiative_id");
  4.1759 +      ELSE
  4.1760 +        IF TG_OP='UPDATE' THEN
  4.1761 +          IF COALESCE(OLD."inititiave_id" != NEW."initiative_id", TRUE) THEN
  4.1762 +            PERFORM "copy_current_draft_data"(OLD."initiative_id");
  4.1763 +          END IF;
  4.1764 +        END IF;
  4.1765 +        PERFORM "copy_current_draft_data"(NEW."initiative_id");
  4.1766 +      END IF;
  4.1767 +      RETURN NULL;
  4.1768 +    END;
  4.1769 +  $$;
  4.1770 +
  4.1771 +CREATE TRIGGER "copy_current_draft_data"
  4.1772 +  AFTER INSERT OR UPDATE OR DELETE ON "draft"
  4.1773 +  FOR EACH ROW EXECUTE PROCEDURE
  4.1774 +  "copy_current_draft_data_trigger"();
  4.1775 +
  4.1776 +COMMENT ON FUNCTION "copy_current_draft_data_trigger"() IS 'Implementation of trigger "copy_current_draft_data" on table "draft"';
  4.1777 +COMMENT ON TRIGGER "copy_current_draft_data" ON "draft" IS 'Copy certain fields from most recent "draft" to "initiative"';
  4.1778 +
  4.1779 +
  4.1780 +CREATE VIEW "area_quorum" AS
  4.1781 +  SELECT
  4.1782 +    "area"."id" AS "area_id",
  4.1783 +    ceil(
  4.1784 +      "area"."quorum_standard"::FLOAT8 * "quorum_factor"::FLOAT8 ^ (
  4.1785 +        coalesce(
  4.1786 +          ( SELECT sum(
  4.1787 +              ( extract(epoch from "area"."quorum_time")::FLOAT8 /
  4.1788 +                extract(epoch from
  4.1789 +                  ("issue"."accepted"-"issue"."created") +
  4.1790 +                  "issue"."discussion_time" +
  4.1791 +                  "issue"."verification_time" +
  4.1792 +                  "issue"."voting_time"
  4.1793 +                )::FLOAT8
  4.1794 +              ) ^ "area"."quorum_exponent"::FLOAT8
  4.1795 +            )
  4.1796 +            FROM "issue" JOIN "policy"
  4.1797 +            ON "issue"."policy_id" = "policy"."id"
  4.1798 +            WHERE "issue"."area_id" = "area"."id"
  4.1799 +            AND "issue"."accepted" NOTNULL
  4.1800 +            AND "issue"."closed" ISNULL
  4.1801 +            AND "policy"."polling" = FALSE
  4.1802 +          )::FLOAT8, 0::FLOAT8
  4.1803 +        ) / "area"."quorum_issues"::FLOAT8 - 1::FLOAT8
  4.1804 +      ) * CASE WHEN "area"."quorum_den" ISNULL THEN 1 ELSE (
  4.1805 +        SELECT "snapshot"."population"
  4.1806 +        FROM "snapshot"
  4.1807 +        WHERE "snapshot"."area_id" = "area"."id"
  4.1808 +        AND "snapshot"."issue_id" ISNULL
  4.1809 +        ORDER BY "snapshot"."id" DESC
  4.1810 +        LIMIT 1
  4.1811 +      ) END / coalesce("area"."quorum_den", 1)
  4.1812 +
  4.1813 +    )::INT4 AS "issue_quorum"
  4.1814 +  FROM "area";
  4.1815 +
  4.1816 +COMMENT ON VIEW "area_quorum" IS 'Area-based quorum considering number of open (accepted) issues';
  4.1817 +
  4.1818 +
  4.1819 +CREATE VIEW "area_with_unaccepted_issues" AS
  4.1820 +  SELECT DISTINCT ON ("area"."id") "area".*
  4.1821 +  FROM "area" JOIN "issue" ON "area"."id" = "issue"."area_id"
  4.1822 +  WHERE "issue"."state" = 'admission';
  4.1823 +
  4.1824 +COMMENT ON VIEW "area_with_unaccepted_issues" IS 'All areas with unaccepted open issues (needed for issue admission system)';
  4.1825 +
  4.1826 +
  4.1827 +DROP VIEW "area_member_count";
  4.1828 +
  4.1829 +
  4.1830 +DROP TABLE "membership";
  4.1831 +
  4.1832 +
  4.1833 +DROP FUNCTION "membership_weight"
  4.1834 +  ( "area_id_p"         "area"."id"%TYPE,
  4.1835 +    "member_id_p"       "member"."id"%TYPE );
  4.1836 +
  4.1837 +
  4.1838 +DROP FUNCTION "membership_weight_with_skipping"
  4.1839 +  ( "area_id_p"         "area"."id"%TYPE,
  4.1840 +    "member_id_p"       "member"."id"%TYPE,
  4.1841 +    "skip_member_ids_p" INT4[] );  -- TODO: ordering/cascade
  4.1842 +
  4.1843 +
  4.1844 +CREATE OR REPLACE VIEW "issue_delegation" AS
  4.1845 +  SELECT DISTINCT ON ("issue"."id", "delegation"."truster_id")
  4.1846 +    "issue"."id" AS "issue_id",
  4.1847 +    "delegation"."id",
  4.1848 +    "delegation"."truster_id",
  4.1849 +    "delegation"."trustee_id",
  4.1850 +    "delegation"."scope"
  4.1851 +  FROM "issue"
  4.1852 +  JOIN "area"
  4.1853 +    ON "area"."id" = "issue"."area_id"
  4.1854 +  JOIN "delegation"
  4.1855 +    ON "delegation"."unit_id" = "area"."unit_id"
  4.1856 +    OR "delegation"."area_id" = "area"."id"
  4.1857 +    OR "delegation"."issue_id" = "issue"."id"
  4.1858 +  JOIN "member"
  4.1859 +    ON "delegation"."truster_id" = "member"."id"
  4.1860 +  JOIN "privilege"
  4.1861 +    ON "area"."unit_id" = "privilege"."unit_id"
  4.1862 +    AND "delegation"."truster_id" = "privilege"."member_id"
  4.1863 +  WHERE "member"."active" AND "privilege"."voting_right"
  4.1864 +  ORDER BY
  4.1865 +    "issue"."id",
  4.1866 +    "delegation"."truster_id",
  4.1867 +    "delegation"."scope" DESC;
  4.1868 +
  4.1869 +
  4.1870 +CREATE VIEW "unit_member" AS
  4.1871 +  SELECT
  4.1872 +    "unit"."id"   AS "unit_id",
  4.1873 +    "member"."id" AS "member_id"
  4.1874 +  FROM "privilege"
  4.1875 +  JOIN "unit"   ON "unit_id"     = "privilege"."unit_id"
  4.1876 +  JOIN "member" ON "member"."id" = "privilege"."member_id"
  4.1877 +  WHERE "privilege"."voting_right" AND "member"."active";
  4.1878 +
  4.1879 +COMMENT ON VIEW "unit_member" IS 'Active members with voting right in a unit';
  4.1880 + 
  4.1881 + 
  4.1882 +CREATE OR REPLACE VIEW "unit_member_count" AS
  4.1883 +  SELECT
  4.1884 +    "unit"."id" AS "unit_id",
  4.1885 +    count("unit_member"."member_id") AS "member_count"
  4.1886 +  FROM "unit" LEFT JOIN "unit_member"
  4.1887 +  ON "unit"."id" = "unit_member"."unit_id"
  4.1888 +  GROUP BY "unit"."id";
  4.1889 + 
  4.1890 +COMMENT ON VIEW "unit_member_count" IS 'View used to update "member_count" column of "unit" table';
  4.1891 +
  4.1892 +
  4.1893 +CREATE OR REPLACE VIEW "opening_draft" AS
  4.1894 +  SELECT DISTINCT ON ("initiative_id") * FROM "draft"
  4.1895 +  ORDER BY "initiative_id", "id";
  4.1896 + 
  4.1897 + 
  4.1898 +CREATE OR REPLACE VIEW "current_draft" AS
  4.1899 +  SELECT DISTINCT ON ("initiative_id") * FROM "draft"
  4.1900 +  ORDER BY "initiative_id", "id" DESC;
  4.1901 + 
  4.1902 +
  4.1903 +CREATE OR REPLACE VIEW "issue_supporter_in_admission_state" AS
  4.1904 +  SELECT
  4.1905 +    "area"."unit_id",
  4.1906 +    "issue"."area_id",
  4.1907 +    "issue"."id" AS "issue_id",
  4.1908 +    "supporter"."member_id",
  4.1909 +    "direct_interest_snapshot"."weight"
  4.1910 +  FROM "issue"
  4.1911 +  JOIN "area" ON "area"."id" = "issue"."area_id"
  4.1912 +  JOIN "supporter" ON "supporter"."issue_id" = "issue"."id"
  4.1913 +  JOIN "direct_interest_snapshot"
  4.1914 +    ON "direct_interest_snapshot"."snapshot_id" = "issue"."latest_snapshot_id"
  4.1915 +    AND "direct_interest_snapshot"."issue_id" = "issue"."id"
  4.1916 +    AND "direct_interest_snapshot"."member_id" = "supporter"."member_id"
  4.1917 +  WHERE "issue"."state" = 'admission'::"issue_state";
  4.1918 +
  4.1919 +
  4.1920 +CREATE OR REPLACE VIEW "individual_suggestion_ranking" AS
  4.1921 +  SELECT
  4.1922 +    "opinion"."initiative_id",
  4.1923 +    "opinion"."member_id",
  4.1924 +    "direct_interest_snapshot"."weight",
  4.1925 +    CASE WHEN
  4.1926 +      ("opinion"."degree" = 2 AND "opinion"."fulfilled" = FALSE) OR
  4.1927 +      ("opinion"."degree" = -2 AND "opinion"."fulfilled" = TRUE)
  4.1928 +    THEN 1 ELSE
  4.1929 +      CASE WHEN
  4.1930 +        ("opinion"."degree" = 1 AND "opinion"."fulfilled" = FALSE) OR
  4.1931 +        ("opinion"."degree" = -1 AND "opinion"."fulfilled" = TRUE)
  4.1932 +      THEN 2 ELSE
  4.1933 +        CASE WHEN
  4.1934 +          ("opinion"."degree" = 2 AND "opinion"."fulfilled" = TRUE) OR
  4.1935 +          ("opinion"."degree" = -2 AND "opinion"."fulfilled" = FALSE)
  4.1936 +        THEN 3 ELSE 4 END
  4.1937 +      END
  4.1938 +    END AS "preference",
  4.1939 +    "opinion"."suggestion_id"
  4.1940 +  FROM "opinion"
  4.1941 +  JOIN "initiative" ON "initiative"."id" = "opinion"."initiative_id"
  4.1942 +  JOIN "issue" ON "issue"."id" = "initiative"."issue_id"
  4.1943 +  JOIN "direct_interest_snapshot"
  4.1944 +    ON "direct_interest_snapshot"."snapshot_id" = "issue"."latest_snapshot_id"
  4.1945 +    AND "direct_interest_snapshot"."issue_id" = "issue"."id"
  4.1946 +    AND "direct_interest_snapshot"."member_id" = "opinion"."member_id";
  4.1947 +
  4.1948 +
  4.1949 +CREATE VIEW "expired_session" AS
  4.1950 +  SELECT * FROM "session" WHERE now() > "expiry";
  4.1951 +
  4.1952 +CREATE RULE "delete" AS ON DELETE TO "expired_session" DO INSTEAD
  4.1953 +  DELETE FROM "session" WHERE "id" = OLD."id";
  4.1954 +
  4.1955 +COMMENT ON VIEW "expired_session" IS 'View containing all expired sessions where DELETE is possible';
  4.1956 +COMMENT ON RULE "delete" ON "expired_session" IS 'Rule allowing DELETE on rows in "expired_session" view, i.e. DELETE FROM "expired_session"';
  4.1957 +
  4.1958 +
  4.1959 +CREATE VIEW "expired_token" AS
  4.1960 +  SELECT * FROM "token" WHERE now() > "expiry" AND NOT (
  4.1961 +    "token_type" = 'authorization' AND "used" AND EXISTS (
  4.1962 +      SELECT NULL FROM "token" AS "other"
  4.1963 +      WHERE "other"."authorization_token_id" = "id" ) );
  4.1964 +
  4.1965 +CREATE RULE "delete" AS ON DELETE TO "expired_token" DO INSTEAD
  4.1966 +  DELETE FROM "token" WHERE "id" = OLD."id";
  4.1967 +
  4.1968 +COMMENT ON VIEW "expired_token" IS 'View containing all expired tokens where DELETE is possible; Note that used authorization codes must not be deleted if still referred to by other tokens';
  4.1969 +
  4.1970 +
  4.1971 +CREATE VIEW "unused_snapshot" AS
  4.1972 +  SELECT "snapshot".* FROM "snapshot"
  4.1973 +  LEFT JOIN "issue"
  4.1974 +  ON "snapshot"."id" = "issue"."latest_snapshot_id"
  4.1975 +  OR "snapshot"."id" = "issue"."admission_snapshot_id"
  4.1976 +  OR "snapshot"."id" = "issue"."half_freeze_snapshot_id"
  4.1977 +  OR "snapshot"."id" = "issue"."full_freeze_snapshot_id"
  4.1978 +  WHERE "issue"."id" ISNULL;
  4.1979 +
  4.1980 +CREATE RULE "delete" AS ON DELETE TO "unused_snapshot" DO INSTEAD
  4.1981 +  DELETE FROM "snapshot" WHERE "id" = OLD."id";
  4.1982 +
  4.1983 +COMMENT ON VIEW "unused_snapshot" IS 'Snapshots that are not referenced by any issue (either as latest snapshot or as snapshot at phase/state change)';
  4.1984 +
  4.1985 +
  4.1986 +CREATE VIEW "expired_snapshot" AS
  4.1987 +  SELECT "unused_snapshot".* FROM "unused_snapshot" CROSS JOIN "system_setting"
  4.1988 +  WHERE "unused_snapshot"."calculated" <
  4.1989 +    now() - "system_setting"."snapshot_retention";
  4.1990 +
  4.1991 +CREATE RULE "delete" AS ON DELETE TO "expired_snapshot" DO INSTEAD
  4.1992 +  DELETE FROM "snapshot" WHERE "id" = OLD."id";
  4.1993 +
  4.1994 +COMMENT ON VIEW "expired_snapshot" IS 'Contains "unused_snapshot"s that are older than "system_setting"."snapshot_retention" (for deletion)';
  4.1995 +
  4.1996 +
  4.1997 +COMMENT ON COLUMN "delegation_chain_row"."participation" IS 'In case of delegation chains for issues: interest; for area and global delegation chains: always null';
  4.1998 +
  4.1999 +
  4.2000 +CREATE OR REPLACE FUNCTION "delegation_chain"
  4.2001 +  ( "member_id_p"           "member"."id"%TYPE,
  4.2002 +    "unit_id_p"             "unit"."id"%TYPE,
  4.2003 +    "area_id_p"             "area"."id"%TYPE,
  4.2004 +    "issue_id_p"            "issue"."id"%TYPE,
  4.2005 +    "simulate_trustee_id_p" "member"."id"%TYPE DEFAULT NULL,
  4.2006 +    "simulate_default_p"    BOOLEAN            DEFAULT FALSE )
  4.2007 +  RETURNS SETOF "delegation_chain_row"
  4.2008 +  LANGUAGE 'plpgsql' STABLE AS $$
  4.2009 +    DECLARE
  4.2010 +      "scope_v"            "delegation_scope";
  4.2011 +      "unit_id_v"          "unit"."id"%TYPE;
  4.2012 +      "area_id_v"          "area"."id"%TYPE;
  4.2013 +      "issue_row"          "issue"%ROWTYPE;
  4.2014 +      "visited_member_ids" INT4[];  -- "member"."id"%TYPE[]
  4.2015 +      "loop_member_id_v"   "member"."id"%TYPE;
  4.2016 +      "output_row"         "delegation_chain_row";
  4.2017 +      "output_rows"        "delegation_chain_row"[];
  4.2018 +      "simulate_v"         BOOLEAN;
  4.2019 +      "simulate_here_v"    BOOLEAN;
  4.2020 +      "delegation_row"     "delegation"%ROWTYPE;
  4.2021 +      "row_count"          INT4;
  4.2022 +      "i"                  INT4;
  4.2023 +      "loop_v"             BOOLEAN;
  4.2024 +    BEGIN
  4.2025 +      IF "simulate_trustee_id_p" NOTNULL AND "simulate_default_p" THEN
  4.2026 +        RAISE EXCEPTION 'Both "simulate_trustee_id_p" is set, and "simulate_default_p" is true';
  4.2027 +      END IF;
  4.2028 +      IF "simulate_trustee_id_p" NOTNULL OR "simulate_default_p" THEN
  4.2029 +        "simulate_v" := TRUE;
  4.2030 +      ELSE
  4.2031 +        "simulate_v" := FALSE;
  4.2032 +      END IF;
  4.2033 +      IF
  4.2034 +        "unit_id_p" NOTNULL AND
  4.2035 +        "area_id_p" ISNULL AND
  4.2036 +        "issue_id_p" ISNULL
  4.2037 +      THEN
  4.2038 +        "scope_v" := 'unit';
  4.2039 +        "unit_id_v" := "unit_id_p";
  4.2040 +      ELSIF
  4.2041 +        "unit_id_p" ISNULL AND
  4.2042 +        "area_id_p" NOTNULL AND
  4.2043 +        "issue_id_p" ISNULL
  4.2044 +      THEN
  4.2045 +        "scope_v" := 'area';
  4.2046 +        "area_id_v" := "area_id_p";
  4.2047 +        SELECT "unit_id" INTO "unit_id_v"
  4.2048 +          FROM "area" WHERE "id" = "area_id_v";
  4.2049 +      ELSIF
  4.2050 +        "unit_id_p" ISNULL AND
  4.2051 +        "area_id_p" ISNULL AND
  4.2052 +        "issue_id_p" NOTNULL
  4.2053 +      THEN
  4.2054 +        SELECT INTO "issue_row" * FROM "issue" WHERE "id" = "issue_id_p";
  4.2055 +        IF "issue_row"."id" ISNULL THEN
  4.2056 +          RETURN;
  4.2057 +        END IF;
  4.2058 +        IF "issue_row"."closed" NOTNULL THEN
  4.2059 +          IF "simulate_v" THEN
  4.2060 +            RAISE EXCEPTION 'Tried to simulate delegation chain for closed issue.';
  4.2061 +          END IF;
  4.2062 +          FOR "output_row" IN
  4.2063 +            SELECT * FROM
  4.2064 +            "delegation_chain_for_closed_issue"("member_id_p", "issue_id_p")
  4.2065 +          LOOP
  4.2066 +            RETURN NEXT "output_row";
  4.2067 +          END LOOP;
  4.2068 +          RETURN;
  4.2069 +        END IF;
  4.2070 +        "scope_v" := 'issue';
  4.2071 +        SELECT "area_id" INTO "area_id_v"
  4.2072 +          FROM "issue" WHERE "id" = "issue_id_p";
  4.2073 +        SELECT "unit_id" INTO "unit_id_v"
  4.2074 +          FROM "area"  WHERE "id" = "area_id_v";
  4.2075 +      ELSE
  4.2076 +        RAISE EXCEPTION 'Exactly one of unit_id_p, area_id_p, or issue_id_p must be NOTNULL.';
  4.2077 +      END IF;
  4.2078 +      "visited_member_ids" := '{}';
  4.2079 +      "loop_member_id_v"   := NULL;
  4.2080 +      "output_rows"        := '{}';
  4.2081 +      "output_row"."index"         := 0;
  4.2082 +      "output_row"."member_id"     := "member_id_p";
  4.2083 +      "output_row"."member_valid"  := TRUE;
  4.2084 +      "output_row"."participation" := FALSE;
  4.2085 +      "output_row"."overridden"    := FALSE;
  4.2086 +      "output_row"."disabled_out"  := FALSE;
  4.2087 +      "output_row"."scope_out"     := NULL;
  4.2088 +      LOOP
  4.2089 +        IF "visited_member_ids" @> ARRAY["output_row"."member_id"] THEN
  4.2090 +          "loop_member_id_v" := "output_row"."member_id";
  4.2091 +        ELSE
  4.2092 +          "visited_member_ids" :=
  4.2093 +            "visited_member_ids" || "output_row"."member_id";
  4.2094 +        END IF;
  4.2095 +        IF "output_row"."participation" ISNULL THEN
  4.2096 +          "output_row"."overridden" := NULL;
  4.2097 +        ELSIF "output_row"."participation" THEN
  4.2098 +          "output_row"."overridden" := TRUE;
  4.2099 +        END IF;
  4.2100 +        "output_row"."scope_in" := "output_row"."scope_out";
  4.2101 +        "output_row"."member_valid" := EXISTS (
  4.2102 +          SELECT NULL FROM "member" JOIN "privilege"
  4.2103 +          ON "privilege"."member_id" = "member"."id"
  4.2104 +          AND "privilege"."unit_id" = "unit_id_v"
  4.2105 +          WHERE "id" = "output_row"."member_id"
  4.2106 +          AND "member"."active" AND "privilege"."voting_right"
  4.2107 +        );
  4.2108 +        "simulate_here_v" := (
  4.2109 +          "simulate_v" AND
  4.2110 +          "output_row"."member_id" = "member_id_p"
  4.2111 +        );
  4.2112 +        "delegation_row" := ROW(NULL);
  4.2113 +        IF "output_row"."member_valid" OR "simulate_here_v" THEN
  4.2114 +          IF "scope_v" = 'unit' THEN
  4.2115 +            IF NOT "simulate_here_v" THEN
  4.2116 +              SELECT * INTO "delegation_row" FROM "delegation"
  4.2117 +                WHERE "truster_id" = "output_row"."member_id"
  4.2118 +                AND "unit_id" = "unit_id_v";
  4.2119 +            END IF;
  4.2120 +          ELSIF "scope_v" = 'area' THEN
  4.2121 +            IF "simulate_here_v" THEN
  4.2122 +              IF "simulate_trustee_id_p" ISNULL THEN
  4.2123 +                SELECT * INTO "delegation_row" FROM "delegation"
  4.2124 +                  WHERE "truster_id" = "output_row"."member_id"
  4.2125 +                  AND "unit_id" = "unit_id_v";
  4.2126 +              END IF;
  4.2127 +            ELSE
  4.2128 +              SELECT * INTO "delegation_row" FROM "delegation"
  4.2129 +                WHERE "truster_id" = "output_row"."member_id"
  4.2130 +                AND (
  4.2131 +                  "unit_id" = "unit_id_v" OR
  4.2132 +                  "area_id" = "area_id_v"
  4.2133 +                )
  4.2134 +                ORDER BY "scope" DESC;
  4.2135 +            END IF;
  4.2136 +          ELSIF "scope_v" = 'issue' THEN
  4.2137 +            IF "issue_row"."fully_frozen" ISNULL THEN
  4.2138 +              "output_row"."participation" := EXISTS (
  4.2139 +                SELECT NULL FROM "interest"
  4.2140 +                WHERE "issue_id" = "issue_id_p"
  4.2141 +                AND "member_id" = "output_row"."member_id"
  4.2142 +              );
  4.2143 +            ELSE
  4.2144 +              IF "output_row"."member_id" = "member_id_p" THEN
  4.2145 +                "output_row"."participation" := EXISTS (
  4.2146 +                  SELECT NULL FROM "direct_voter"
  4.2147 +                  WHERE "issue_id" = "issue_id_p"
  4.2148 +                  AND "member_id" = "output_row"."member_id"
  4.2149 +                );
  4.2150 +              ELSE
  4.2151 +                "output_row"."participation" := NULL;
  4.2152 +              END IF;
  4.2153 +            END IF;
  4.2154 +            IF "simulate_here_v" THEN
  4.2155 +              IF "simulate_trustee_id_p" ISNULL THEN
  4.2156 +                SELECT * INTO "delegation_row" FROM "delegation"
  4.2157 +                  WHERE "truster_id" = "output_row"."member_id"
  4.2158 +                  AND (
  4.2159 +                    "unit_id" = "unit_id_v" OR
  4.2160 +                    "area_id" = "area_id_v"
  4.2161 +                  )
  4.2162 +                  ORDER BY "scope" DESC;
  4.2163 +              END IF;
  4.2164 +            ELSE
  4.2165 +              SELECT * INTO "delegation_row" FROM "delegation"
  4.2166 +                WHERE "truster_id" = "output_row"."member_id"
  4.2167 +                AND (
  4.2168 +                  "unit_id" = "unit_id_v" OR
  4.2169 +                  "area_id" = "area_id_v" OR
  4.2170 +                  "issue_id" = "issue_id_p"
  4.2171 +                )
  4.2172 +                ORDER BY "scope" DESC;
  4.2173 +            END IF;
  4.2174 +          END IF;
  4.2175 +        ELSE
  4.2176 +          "output_row"."participation" := FALSE;
  4.2177 +        END IF;
  4.2178 +        IF "simulate_here_v" AND "simulate_trustee_id_p" NOTNULL THEN
  4.2179 +          "output_row"."scope_out" := "scope_v";
  4.2180 +          "output_rows" := "output_rows" || "output_row";
  4.2181 +          "output_row"."member_id" := "simulate_trustee_id_p";
  4.2182 +        ELSIF "delegation_row"."trustee_id" NOTNULL THEN
  4.2183 +          "output_row"."scope_out" := "delegation_row"."scope";
  4.2184 +          "output_rows" := "output_rows" || "output_row";
  4.2185 +          "output_row"."member_id" := "delegation_row"."trustee_id";
  4.2186 +        ELSIF "delegation_row"."scope" NOTNULL THEN
  4.2187 +          "output_row"."scope_out" := "delegation_row"."scope";
  4.2188 +          "output_row"."disabled_out" := TRUE;
  4.2189 +          "output_rows" := "output_rows" || "output_row";
  4.2190 +          EXIT;
  4.2191 +        ELSE
  4.2192 +          "output_row"."scope_out" := NULL;
  4.2193 +          "output_rows" := "output_rows" || "output_row";
  4.2194 +          EXIT;
  4.2195 +        END IF;
  4.2196 +        EXIT WHEN "loop_member_id_v" NOTNULL;
  4.2197 +        "output_row"."index" := "output_row"."index" + 1;
  4.2198 +      END LOOP;
  4.2199 +      "row_count" := array_upper("output_rows", 1);
  4.2200 +      "i"      := 1;
  4.2201 +      "loop_v" := FALSE;
  4.2202 +      LOOP
  4.2203 +        "output_row" := "output_rows"["i"];
  4.2204 +        EXIT WHEN "output_row" ISNULL;  -- NOTE: ISNULL and NOT ... NOTNULL produce different results!
  4.2205 +        IF "loop_v" THEN
  4.2206 +          IF "i" + 1 = "row_count" THEN
  4.2207 +            "output_row"."loop" := 'last';
  4.2208 +          ELSIF "i" = "row_count" THEN
  4.2209 +            "output_row"."loop" := 'repetition';
  4.2210 +          ELSE
  4.2211 +            "output_row"."loop" := 'intermediate';
  4.2212 +          END IF;
  4.2213 +        ELSIF "output_row"."member_id" = "loop_member_id_v" THEN
  4.2214 +          "output_row"."loop" := 'first';
  4.2215 +          "loop_v" := TRUE;
  4.2216 +        END IF;
  4.2217 +        IF "scope_v" = 'unit' THEN
  4.2218 +          "output_row"."participation" := NULL;
  4.2219 +        END IF;
  4.2220 +        RETURN NEXT "output_row";
  4.2221 +        "i" := "i" + 1;
  4.2222 +      END LOOP;
  4.2223 +      RETURN;
  4.2224 +    END;
  4.2225 +  $$;
  4.2226 +
  4.2227 +
  4.2228 +CREATE OR REPLACE FUNCTION "get_initiatives_for_notification"
  4.2229 +  ( "recipient_id_p" "member"."id"%TYPE )
  4.2230 +  RETURNS SETOF "initiative_for_notification"
  4.2231 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2232 +    DECLARE
  4.2233 +      "result_row"           "initiative_for_notification"%ROWTYPE;
  4.2234 +      "last_draft_id_v"      "draft"."id"%TYPE;
  4.2235 +      "last_suggestion_id_v" "suggestion"."id"%TYPE;
  4.2236 +    BEGIN
  4.2237 +      PERFORM "require_transaction_isolation"();
  4.2238 +      PERFORM NULL FROM "member" WHERE "id" = "recipient_id_p" FOR UPDATE;
  4.2239 +      FOR "result_row" IN
  4.2240 +        SELECT * FROM "initiative_for_notification"
  4.2241 +        WHERE "recipient_id" = "recipient_id_p"
  4.2242 +      LOOP
  4.2243 +        SELECT "id" INTO "last_draft_id_v" FROM "draft"
  4.2244 +          WHERE "draft"."initiative_id" = "result_row"."initiative_id"
  4.2245 +          ORDER BY "id" DESC LIMIT 1;
  4.2246 +        SELECT "id" INTO "last_suggestion_id_v" FROM "suggestion"
  4.2247 +          WHERE "suggestion"."initiative_id" = "result_row"."initiative_id"
  4.2248 +          ORDER BY "id" DESC LIMIT 1;
  4.2249 +        INSERT INTO "notification_initiative_sent"
  4.2250 +          ("member_id", "initiative_id", "last_draft_id", "last_suggestion_id")
  4.2251 +          VALUES (
  4.2252 +            "recipient_id_p",
  4.2253 +            "result_row"."initiative_id",
  4.2254 +            "last_draft_id_v",
  4.2255 +            "last_suggestion_id_v" )
  4.2256 +          ON CONFLICT ("member_id", "initiative_id") DO UPDATE SET
  4.2257 +            "last_draft_id" = "last_draft_id_v",
  4.2258 +            "last_suggestion_id" = "last_suggestion_id_v";
  4.2259 +        RETURN NEXT "result_row";
  4.2260 +      END LOOP;
  4.2261 +      DELETE FROM "notification_initiative_sent"
  4.2262 +        USING "initiative", "issue"
  4.2263 +        WHERE "notification_initiative_sent"."member_id" = "recipient_id_p"
  4.2264 +        AND "initiative"."id" = "notification_initiative_sent"."initiative_id"
  4.2265 +        AND "issue"."id" = "initiative"."issue_id"
  4.2266 +        AND ( "issue"."closed" NOTNULL OR "issue"."fully_frozen" NOTNULL );
  4.2267 +      UPDATE "member" SET
  4.2268 +        "notification_counter" = "notification_counter" + 1,
  4.2269 +        "notification_sent" = now()
  4.2270 +        WHERE "id" = "recipient_id_p";
  4.2271 +      RETURN;
  4.2272 +    END;
  4.2273 +  $$;
  4.2274 +
  4.2275 +
  4.2276 +CREATE OR REPLACE FUNCTION "calculate_member_counts"()
  4.2277 +  RETURNS VOID
  4.2278 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2279 +    BEGIN
  4.2280 +      PERFORM "require_transaction_isolation"();
  4.2281 +      DELETE FROM "member_count";
  4.2282 +      INSERT INTO "member_count" ("total_count")
  4.2283 +        SELECT "total_count" FROM "member_count_view";
  4.2284 +      UPDATE "unit" SET "member_count" = "view"."member_count"
  4.2285 +        FROM "unit_member_count" AS "view"
  4.2286 +        WHERE "view"."unit_id" = "unit"."id";
  4.2287 +      RETURN;
  4.2288 +    END;
  4.2289 +  $$;
  4.2290 +
  4.2291 +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 "unit_member_count"';
  4.2292 +
  4.2293 +
  4.2294 +CREATE FUNCTION "calculate_area_quorum"()
  4.2295 +  RETURNS VOID
  4.2296 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2297 +    BEGIN
  4.2298 +      PERFORM "dont_require_transaction_isolation"();
  4.2299 +      UPDATE "area" SET "issue_quorum" = "view"."issue_quorum"
  4.2300 +        FROM "area_quorum" AS "view"
  4.2301 +        WHERE "view"."area_id" = "area"."id";
  4.2302 +      RETURN;
  4.2303 +    END;
  4.2304 +  $$;
  4.2305 +
  4.2306 +COMMENT ON FUNCTION "calculate_area_quorum"() IS 'Calculate column "issue_quorum" in table "area" from view "area_quorum"';
  4.2307 +
  4.2308 +
  4.2309 +DROP VIEW "remaining_harmonic_initiative_weight_summands";
  4.2310 +DROP VIEW "remaining_harmonic_supporter_weight";
  4.2311 +
  4.2312 +
  4.2313 +CREATE VIEW "remaining_harmonic_supporter_weight" AS
  4.2314 +  SELECT
  4.2315 +    "direct_interest_snapshot"."snapshot_id",
  4.2316 +    "direct_interest_snapshot"."issue_id",
  4.2317 +    "direct_interest_snapshot"."member_id",
  4.2318 +    "direct_interest_snapshot"."weight" AS "weight_num",
  4.2319 +    count("initiative"."id") AS "weight_den"
  4.2320 +  FROM "issue"
  4.2321 +  JOIN "direct_interest_snapshot"
  4.2322 +    ON "issue"."latest_snapshot_id" = "direct_interest_snapshot"."snapshot_id"
  4.2323 +    AND "issue"."id" = "direct_interest_snapshot"."issue_id"
  4.2324 +  JOIN "initiative"
  4.2325 +    ON "issue"."id" = "initiative"."issue_id"
  4.2326 +    AND "initiative"."harmonic_weight" ISNULL
  4.2327 +  JOIN "direct_supporter_snapshot"
  4.2328 +    ON "issue"."latest_snapshot_id" = "direct_supporter_snapshot"."snapshot_id"
  4.2329 +    AND "initiative"."id" = "direct_supporter_snapshot"."initiative_id"
  4.2330 +    AND "direct_interest_snapshot"."member_id" = "direct_supporter_snapshot"."member_id"
  4.2331 +    AND (
  4.2332 +      "direct_supporter_snapshot"."satisfied" = TRUE OR
  4.2333 +      coalesce("initiative"."admitted", FALSE) = FALSE
  4.2334 +    )
  4.2335 +  GROUP BY
  4.2336 +    "direct_interest_snapshot"."snapshot_id",
  4.2337 +    "direct_interest_snapshot"."issue_id",
  4.2338 +    "direct_interest_snapshot"."member_id",
  4.2339 +    "direct_interest_snapshot"."weight";
  4.2340 +
  4.2341 +
  4.2342 +CREATE VIEW "remaining_harmonic_initiative_weight_summands" AS
  4.2343 +  SELECT
  4.2344 +    "initiative"."issue_id",
  4.2345 +    "initiative"."id" AS "initiative_id",
  4.2346 +    "initiative"."admitted",
  4.2347 +    sum("remaining_harmonic_supporter_weight"."weight_num") AS "weight_num",
  4.2348 +    "remaining_harmonic_supporter_weight"."weight_den"
  4.2349 +  FROM "remaining_harmonic_supporter_weight"
  4.2350 +  JOIN "initiative"
  4.2351 +    ON "remaining_harmonic_supporter_weight"."issue_id" = "initiative"."issue_id"
  4.2352 +    AND "initiative"."harmonic_weight" ISNULL
  4.2353 +  JOIN "direct_supporter_snapshot"
  4.2354 +    ON "remaining_harmonic_supporter_weight"."snapshot_id" = "direct_supporter_snapshot"."snapshot_id"
  4.2355 +    AND "initiative"."id" = "direct_supporter_snapshot"."initiative_id"
  4.2356 +    AND "remaining_harmonic_supporter_weight"."member_id" = "direct_supporter_snapshot"."member_id"
  4.2357 +    AND (
  4.2358 +      "direct_supporter_snapshot"."satisfied" = TRUE OR
  4.2359 +      coalesce("initiative"."admitted", FALSE) = FALSE
  4.2360 +    )
  4.2361 +  GROUP BY
  4.2362 +    "initiative"."issue_id",
  4.2363 +    "initiative"."id",
  4.2364 +    "initiative"."admitted",
  4.2365 +    "remaining_harmonic_supporter_weight"."weight_den";
  4.2366 +
  4.2367 +
  4.2368 +DROP FUNCTION "create_population_snapshot"
  4.2369 +  ( "issue_id_p" "issue"."id"%TYPE );
  4.2370 +
  4.2371 +
  4.2372 +DROP FUNCTION "weight_of_added_delegations_for_population_snapshot"
  4.2373 +  ( "issue_id_p"            "issue"."id"%TYPE,
  4.2374 +    "member_id_p"           "member"."id"%TYPE,
  4.2375 +    "delegate_member_ids_p" "delegating_population_snapshot"."delegate_member_ids"%TYPE );
  4.2376 +
  4.2377 +
  4.2378 +DROP FUNCTION "weight_of_added_delegations_for_interest_snapshot"
  4.2379 +  ( "issue_id_p"            "issue"."id"%TYPE,
  4.2380 +    "member_id_p"           "member"."id"%TYPE,
  4.2381 +    "delegate_member_ids_p" "delegating_interest_snapshot"."delegate_member_ids"%TYPE );
  4.2382 +
  4.2383 +
  4.2384 +CREATE FUNCTION "weight_of_added_delegations_for_snapshot"
  4.2385 +  ( "snapshot_id_p"         "snapshot"."id"%TYPE,
  4.2386 +    "issue_id_p"            "issue"."id"%TYPE,
  4.2387 +    "member_id_p"           "member"."id"%TYPE,
  4.2388 +    "delegate_member_ids_p" "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
  4.2389 +  RETURNS "direct_interest_snapshot"."weight"%TYPE
  4.2390 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2391 +    DECLARE
  4.2392 +      "issue_delegation_row"  "issue_delegation"%ROWTYPE;
  4.2393 +      "delegate_member_ids_v" "delegating_interest_snapshot"."delegate_member_ids"%TYPE;
  4.2394 +      "weight_v"              INT4;
  4.2395 +      "sub_weight_v"          INT4;
  4.2396 +    BEGIN
  4.2397 +      PERFORM "require_transaction_isolation"();
  4.2398 +      "weight_v" := 0;
  4.2399 +      FOR "issue_delegation_row" IN
  4.2400 +        SELECT * FROM "issue_delegation"
  4.2401 +        WHERE "trustee_id" = "member_id_p"
  4.2402 +        AND "issue_id" = "issue_id_p"
  4.2403 +      LOOP
  4.2404 +        IF NOT EXISTS (
  4.2405 +          SELECT NULL FROM "direct_interest_snapshot"
  4.2406 +          WHERE "snapshot_id" = "snapshot_id_p"
  4.2407 +          AND "issue_id" = "issue_id_p"
  4.2408 +          AND "member_id" = "issue_delegation_row"."truster_id"
  4.2409 +        ) AND NOT EXISTS (
  4.2410 +          SELECT NULL FROM "delegating_interest_snapshot"
  4.2411 +          WHERE "snapshot_id" = "snapshot_id_p"
  4.2412 +          AND "issue_id" = "issue_id_p"
  4.2413 +          AND "member_id" = "issue_delegation_row"."truster_id"
  4.2414 +        ) THEN
  4.2415 +          "delegate_member_ids_v" :=
  4.2416 +            "member_id_p" || "delegate_member_ids_p";
  4.2417 +          INSERT INTO "delegating_interest_snapshot" (
  4.2418 +              "snapshot_id",
  4.2419 +              "issue_id",
  4.2420 +              "member_id",
  4.2421 +              "scope",
  4.2422 +              "delegate_member_ids"
  4.2423 +            ) VALUES (
  4.2424 +              "snapshot_id_p",
  4.2425 +              "issue_id_p",
  4.2426 +              "issue_delegation_row"."truster_id",
  4.2427 +              "issue_delegation_row"."scope",
  4.2428 +              "delegate_member_ids_v"
  4.2429 +            );
  4.2430 +          "sub_weight_v" := 1 +
  4.2431 +            "weight_of_added_delegations_for_snapshot"(
  4.2432 +              "snapshot_id_p",
  4.2433 +              "issue_id_p",
  4.2434 +              "issue_delegation_row"."truster_id",
  4.2435 +              "delegate_member_ids_v"
  4.2436 +            );
  4.2437 +          UPDATE "delegating_interest_snapshot"
  4.2438 +            SET "weight" = "sub_weight_v"
  4.2439 +            WHERE "snapshot_id" = "snapshot_id_p"
  4.2440 +            AND "issue_id" = "issue_id_p"
  4.2441 +            AND "member_id" = "issue_delegation_row"."truster_id";
  4.2442 +          "weight_v" := "weight_v" + "sub_weight_v";
  4.2443 +        END IF;
  4.2444 +      END LOOP;
  4.2445 +      RETURN "weight_v";
  4.2446 +    END;
  4.2447 +  $$;
  4.2448 +
  4.2449 +COMMENT ON FUNCTION "weight_of_added_delegations_for_snapshot"
  4.2450 +  ( "snapshot"."id"%TYPE,
  4.2451 +    "issue"."id"%TYPE,
  4.2452 +    "member"."id"%TYPE,
  4.2453 +    "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
  4.2454 +  IS 'Helper function for "fill_snapshot" function';
  4.2455 +
  4.2456 +
  4.2457 +DROP FUNCTION "create_interest_snapshot"
  4.2458 +  ( "issue_id_p" "issue"."id"%TYPE );
  4.2459 +
  4.2460 +
  4.2461 +DROP FUNCTION "create_snapshot"
  4.2462 +  ( "issue_id_p" "issue"."id"%TYPE );
  4.2463 +
  4.2464 +
  4.2465 +CREATE FUNCTION "take_snapshot"
  4.2466 +  ( "issue_id_p" "issue"."id"%TYPE,
  4.2467 +    "area_id_p"  "area"."id"%TYPE = NULL )
  4.2468 +  RETURNS "snapshot"."id"%TYPE
  4.2469 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2470 +    DECLARE
  4.2471 +      "area_id_v"     "area"."id"%TYPE;
  4.2472 +      "unit_id_v"     "unit"."id"%TYPE;
  4.2473 +      "snapshot_id_v" "snapshot"."id"%TYPE;
  4.2474 +      "issue_id_v"    "issue"."id"%TYPE;
  4.2475 +      "member_id_v"   "member"."id"%TYPE;
  4.2476 +    BEGIN
  4.2477 +      IF "issue_id_p" NOTNULL AND "area_id_p" NOTNULL THEN
  4.2478 +        RAISE EXCEPTION 'One of "issue_id_p" and "area_id_p" must be NULL';
  4.2479 +      END IF;
  4.2480 +      PERFORM "require_transaction_isolation"();
  4.2481 +      IF "issue_id_p" ISNULL THEN
  4.2482 +        "area_id_v" := "area_id_p";
  4.2483 +      ELSE
  4.2484 +        SELECT "area_id" INTO "area_id_v"
  4.2485 +          FROM "issue" WHERE "id" = "issue_id_p";
  4.2486 +      END IF;
  4.2487 +      SELECT "unit_id" INTO "unit_id_v" FROM "area" WHERE "id" = "area_id_p";
  4.2488 +      INSERT INTO "snapshot" ("area_id", "issue_id")
  4.2489 +        VALUES ("area_id_v", "issue_id_p")
  4.2490 +        RETURNING "id" INTO "snapshot_id_v";
  4.2491 +      INSERT INTO "snapshot_population" ("snapshot_id", "member_id")
  4.2492 +        SELECT "snapshot_id_v", "member_id"
  4.2493 +        FROM "unit_member" WHERE "unit_id" = "unit_id_v";
  4.2494 +      UPDATE "snapshot" SET
  4.2495 +        "population" = (
  4.2496 +          SELECT count(1) FROM "snapshot_population"
  4.2497 +          WHERE "snapshot_id" = "snapshot_id_v"
  4.2498 +        ) WHERE "id" = "snapshot_id_v";
  4.2499 +      FOR "issue_id_v" IN
  4.2500 +        SELECT "id" FROM "issue"
  4.2501 +        WHERE CASE WHEN "issue_id_p" ISNULL THEN
  4.2502 +          "area_id" = "area_id_p" AND
  4.2503 +          "state" = 'admission'
  4.2504 +        ELSE
  4.2505 +          "id" = "issue_id_p"
  4.2506 +        END
  4.2507 +      LOOP
  4.2508 +        INSERT INTO "snapshot_issue" ("snapshot_id", "issue_id")
  4.2509 +          VALUES ("snapshot_id_v", "issue_id_v");
  4.2510 +        INSERT INTO "direct_interest_snapshot"
  4.2511 +          ("snapshot_id", "issue_id", "member_id")
  4.2512 +          SELECT
  4.2513 +            "snapshot_id_v" AS "snapshot_id",
  4.2514 +            "issue_id_v"    AS "issue_id",
  4.2515 +            "member"."id"   AS "member_id"
  4.2516 +          FROM "issue"
  4.2517 +          JOIN "area" ON "issue"."area_id" = "area"."id"
  4.2518 +          JOIN "interest" ON "issue"."id" = "interest"."issue_id"
  4.2519 +          JOIN "member" ON "interest"."member_id" = "member"."id"
  4.2520 +          JOIN "privilege"
  4.2521 +            ON "privilege"."unit_id" = "area"."unit_id"
  4.2522 +            AND "privilege"."member_id" = "member"."id"
  4.2523 +          WHERE "issue"."id" = "issue_id_v"
  4.2524 +          AND "member"."active" AND "privilege"."voting_right";
  4.2525 +        FOR "member_id_v" IN
  4.2526 +          SELECT "member_id" FROM "direct_interest_snapshot"
  4.2527 +          WHERE "snapshot_id" = "snapshot_id_v"
  4.2528 +          AND "issue_id" = "issue_id_v"
  4.2529 +        LOOP
  4.2530 +          UPDATE "direct_interest_snapshot" SET
  4.2531 +            "weight" = 1 +
  4.2532 +              "weight_of_added_delegations_for_snapshot"(
  4.2533 +                "snapshot_id_v",
  4.2534 +                "issue_id_v",
  4.2535 +                "member_id_v",
  4.2536 +                '{}'
  4.2537 +              )
  4.2538 +            WHERE "snapshot_id" = "snapshot_id_v"
  4.2539 +            AND "issue_id" = "issue_id_v"
  4.2540 +            AND "member_id" = "member_id_v";
  4.2541 +        END LOOP;
  4.2542 +        INSERT INTO "direct_supporter_snapshot"
  4.2543 +          ( "snapshot_id", "issue_id", "initiative_id", "member_id",
  4.2544 +            "draft_id", "informed", "satisfied" )
  4.2545 +          SELECT
  4.2546 +            "snapshot_id_v"         AS "snapshot_id",
  4.2547 +            "issue_id_v"            AS "issue_id",
  4.2548 +            "initiative"."id"       AS "initiative_id",
  4.2549 +            "supporter"."member_id" AS "member_id",
  4.2550 +            "supporter"."draft_id"  AS "draft_id",
  4.2551 +            "supporter"."draft_id" = "current_draft"."id" AS "informed",
  4.2552 +            NOT EXISTS (
  4.2553 +              SELECT NULL FROM "critical_opinion"
  4.2554 +              WHERE "initiative_id" = "initiative"."id"
  4.2555 +              AND "member_id" = "supporter"."member_id"
  4.2556 +            ) AS "satisfied"
  4.2557 +          FROM "initiative"
  4.2558 +          JOIN "supporter"
  4.2559 +          ON "supporter"."initiative_id" = "initiative"."id"
  4.2560 +          JOIN "current_draft"
  4.2561 +          ON "initiative"."id" = "current_draft"."initiative_id"
  4.2562 +          JOIN "direct_interest_snapshot"
  4.2563 +          ON "snapshot_id_v" = "direct_interest_snapshot"."snapshot_id"
  4.2564 +          AND "supporter"."member_id" = "direct_interest_snapshot"."member_id"
  4.2565 +          AND "initiative"."issue_id" = "direct_interest_snapshot"."issue_id"
  4.2566 +          WHERE "initiative"."issue_id" = "issue_id_v";
  4.2567 +        DELETE FROM "temporary_suggestion_counts";
  4.2568 +        INSERT INTO "temporary_suggestion_counts"
  4.2569 +          ( "id",
  4.2570 +            "minus2_unfulfilled_count", "minus2_fulfilled_count",
  4.2571 +            "minus1_unfulfilled_count", "minus1_fulfilled_count",
  4.2572 +            "plus1_unfulfilled_count", "plus1_fulfilled_count",
  4.2573 +            "plus2_unfulfilled_count", "plus2_fulfilled_count" )
  4.2574 +          SELECT
  4.2575 +            "suggestion"."id",
  4.2576 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2577 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2578 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2579 +              AND "di"."issue_id" = "issue_id_v"
  4.2580 +              AND "di"."member_id" = "opinion"."member_id"
  4.2581 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2582 +              AND "opinion"."degree" = -2
  4.2583 +              AND "opinion"."fulfilled" = FALSE
  4.2584 +            ) AS "minus2_unfulfilled_count",
  4.2585 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2586 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2587 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2588 +              AND "di"."issue_id" = "issue_id_v"
  4.2589 +              AND "di"."member_id" = "opinion"."member_id"
  4.2590 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2591 +              AND "opinion"."degree" = -2
  4.2592 +              AND "opinion"."fulfilled" = TRUE
  4.2593 +            ) AS "minus2_fulfilled_count",
  4.2594 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2595 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2596 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2597 +              AND "di"."issue_id" = "issue_id_v"
  4.2598 +              AND "di"."member_id" = "opinion"."member_id"
  4.2599 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2600 +              AND "opinion"."degree" = -1
  4.2601 +              AND "opinion"."fulfilled" = FALSE
  4.2602 +            ) AS "minus1_unfulfilled_count",
  4.2603 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2604 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2605 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2606 +              AND "di"."issue_id" = "issue_id_v"
  4.2607 +              AND "di"."member_id" = "opinion"."member_id"
  4.2608 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2609 +              AND "opinion"."degree" = -1
  4.2610 +              AND "opinion"."fulfilled" = TRUE
  4.2611 +            ) AS "minus1_fulfilled_count",
  4.2612 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2613 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2614 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2615 +              AND "di"."issue_id" = "issue_id_v"
  4.2616 +              AND "di"."member_id" = "opinion"."member_id"
  4.2617 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2618 +              AND "opinion"."degree" = 1
  4.2619 +              AND "opinion"."fulfilled" = FALSE
  4.2620 +            ) AS "plus1_unfulfilled_count",
  4.2621 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2622 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2623 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2624 +              AND "di"."issue_id" = "issue_id_v"
  4.2625 +              AND "di"."member_id" = "opinion"."member_id"
  4.2626 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2627 +              AND "opinion"."degree" = 1
  4.2628 +              AND "opinion"."fulfilled" = TRUE
  4.2629 +            ) AS "plus1_fulfilled_count",
  4.2630 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2631 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2632 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2633 +              AND "di"."issue_id" = "issue_id_v"
  4.2634 +              AND "di"."member_id" = "opinion"."member_id"
  4.2635 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2636 +              AND "opinion"."degree" = 2
  4.2637 +              AND "opinion"."fulfilled" = FALSE
  4.2638 +            ) AS "plus2_unfulfilled_count",
  4.2639 +            ( SELECT coalesce(sum("di"."weight"), 0)
  4.2640 +              FROM "opinion" JOIN "direct_interest_snapshot" AS "di"
  4.2641 +              ON "di"."snapshot_id" = "snapshot_id_v"
  4.2642 +              AND "di"."issue_id" = "issue_id_v"
  4.2643 +              AND "di"."member_id" = "opinion"."member_id"
  4.2644 +              WHERE "opinion"."suggestion_id" = "suggestion"."id"
  4.2645 +              AND "opinion"."degree" = 2
  4.2646 +              AND "opinion"."fulfilled" = TRUE
  4.2647 +            ) AS "plus2_fulfilled_count"
  4.2648 +            FROM "suggestion" JOIN "initiative"
  4.2649 +            ON "suggestion"."initiative_id" = "initiative"."id"
  4.2650 +            WHERE "initiative"."issue_id" = "issue_id_v";
  4.2651 +      END LOOP;
  4.2652 +      RETURN "snapshot_id_v";
  4.2653 +    END;
  4.2654 +  $$;
  4.2655 +
  4.2656 +COMMENT ON FUNCTION "take_snapshot"
  4.2657 +  ( "issue"."id"%TYPE,
  4.2658 +    "area"."id"%TYPE )
  4.2659 +  IS 'This function creates a new interest/supporter snapshot of a particular issue, or, if the first argument is NULL, for all issues in ''admission'' phase of the area given as second argument. It must be executed with TRANSACTION ISOLATION LEVEL REPEATABLE READ. The snapshot must later be finished by calling "finish_snapshot" for every issue.';
  4.2660 +
  4.2661 +
  4.2662 +DROP FUNCTION "set_snapshot_event"
  4.2663 +  ( "issue_id_p" "issue"."id"%TYPE,
  4.2664 +    "event_p" "snapshot_event" );
  4.2665 +
  4.2666 +
  4.2667 +CREATE FUNCTION "finish_snapshot"
  4.2668 +  ( "issue_id_p" "issue"."id"%TYPE )
  4.2669 +  RETURNS VOID
  4.2670 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2671 +    DECLARE
  4.2672 +      "snapshot_id_v" "snapshot"."id"%TYPE;
  4.2673 +    BEGIN
  4.2674 +      -- NOTE: function does not require snapshot isolation but we don't call
  4.2675 +      --       "dont_require_snapshot_isolation" here because this function is
  4.2676 +      --       also invoked by "check_issue"
  4.2677 +      LOCK TABLE "snapshot" IN EXCLUSIVE MODE;
  4.2678 +      SELECT "id" INTO "snapshot_id_v" FROM "snapshot"
  4.2679 +        ORDER BY "id" DESC LIMIT 1;
  4.2680 +      UPDATE "issue" SET
  4.2681 +        "calculated" = "snapshot"."calculated",
  4.2682 +        "latest_snapshot_id" = "snapshot_id_v",
  4.2683 +        "population" = "snapshot"."population"
  4.2684 +        FROM "snapshot"
  4.2685 +        WHERE "issue"."id" = "issue_id_p"
  4.2686 +        AND "snapshot"."id" = "snapshot_id_v";
  4.2687 +      UPDATE "initiative" SET
  4.2688 +        "supporter_count" = (
  4.2689 +          SELECT coalesce(sum("di"."weight"), 0)
  4.2690 +          FROM "direct_interest_snapshot" AS "di"
  4.2691 +          JOIN "direct_supporter_snapshot" AS "ds"
  4.2692 +          ON "di"."member_id" = "ds"."member_id"
  4.2693 +          WHERE "di"."snapshot_id" = "snapshot_id_v"
  4.2694 +          AND "di"."issue_id" = "issue_id_p"
  4.2695 +          AND "ds"."snapshot_id" = "snapshot_id_v"
  4.2696 +          AND "ds"."initiative_id" = "initiative"."id"
  4.2697 +        ),
  4.2698 +        "informed_supporter_count" = (
  4.2699 +          SELECT coalesce(sum("di"."weight"), 0)
  4.2700 +          FROM "direct_interest_snapshot" AS "di"
  4.2701 +          JOIN "direct_supporter_snapshot" AS "ds"
  4.2702 +          ON "di"."member_id" = "ds"."member_id"
  4.2703 +          WHERE "di"."snapshot_id" = "snapshot_id_v"
  4.2704 +          AND "di"."issue_id" = "issue_id_p"
  4.2705 +          AND "ds"."snapshot_id" = "snapshot_id_v"
  4.2706 +          AND "ds"."initiative_id" = "initiative"."id"
  4.2707 +          AND "ds"."informed"
  4.2708 +        ),
  4.2709 +        "satisfied_supporter_count" = (
  4.2710 +          SELECT coalesce(sum("di"."weight"), 0)
  4.2711 +          FROM "direct_interest_snapshot" AS "di"
  4.2712 +          JOIN "direct_supporter_snapshot" AS "ds"
  4.2713 +          ON "di"."member_id" = "ds"."member_id"
  4.2714 +          WHERE "di"."snapshot_id" = "snapshot_id_v"
  4.2715 +          AND "di"."issue_id" = "issue_id_p"
  4.2716 +          AND "ds"."snapshot_id" = "snapshot_id_v"
  4.2717 +          AND "ds"."initiative_id" = "initiative"."id"
  4.2718 +          AND "ds"."satisfied"
  4.2719 +        ),
  4.2720 +        "satisfied_informed_supporter_count" = (
  4.2721 +          SELECT coalesce(sum("di"."weight"), 0)
  4.2722 +          FROM "direct_interest_snapshot" AS "di"
  4.2723 +          JOIN "direct_supporter_snapshot" AS "ds"
  4.2724 +          ON "di"."member_id" = "ds"."member_id"
  4.2725 +          WHERE "di"."snapshot_id" = "snapshot_id_v"
  4.2726 +          AND "di"."issue_id" = "issue_id_p"
  4.2727 +          AND "ds"."snapshot_id" = "snapshot_id_v"
  4.2728 +          AND "ds"."initiative_id" = "initiative"."id"
  4.2729 +          AND "ds"."informed"
  4.2730 +          AND "ds"."satisfied"
  4.2731 +        )
  4.2732 +        WHERE "issue_id" = "issue_id_p";
  4.2733 +      UPDATE "suggestion" SET
  4.2734 +        "minus2_unfulfilled_count" = "temp"."minus2_unfulfilled_count",
  4.2735 +        "minus2_fulfilled_count"   = "temp"."minus2_fulfilled_count",
  4.2736 +        "minus1_unfulfilled_count" = "temp"."minus1_unfulfilled_count",
  4.2737 +        "minus1_fulfilled_count"   = "temp"."minus1_fulfilled_count",
  4.2738 +        "plus1_unfulfilled_count"  = "temp"."plus1_unfulfilled_count",
  4.2739 +        "plus1_fulfilled_count"    = "temp"."plus1_fulfilled_count",
  4.2740 +        "plus2_unfulfilled_count"  = "temp"."plus2_unfulfilled_count",
  4.2741 +        "plus2_fulfilled_count"    = "temp"."plus2_fulfilled_count"
  4.2742 +        FROM "temporary_suggestion_counts" AS "temp", "initiative"
  4.2743 +        WHERE "temp"."id" = "suggestion"."id"
  4.2744 +        AND "initiative"."issue_id" = "issue_id_p"
  4.2745 +        AND "suggestion"."initiative_id" = "initiative"."id";
  4.2746 +      DELETE FROM "temporary_suggestion_counts";
  4.2747 +      RETURN;
  4.2748 +    END;
  4.2749 +  $$;
  4.2750 +
  4.2751 +COMMENT ON FUNCTION "finish_snapshot"
  4.2752 +  ( "issue"."id"%TYPE )
  4.2753 +  IS 'After calling "take_snapshot", this function "finish_snapshot" needs to be called for every issue in the snapshot (separate function calls keep locking time minimal)';
  4.2754 +
  4.2755 + 
  4.2756 +CREATE FUNCTION "issue_admission"
  4.2757 +  ( "area_id_p" "area"."id"%TYPE )
  4.2758 +  RETURNS BOOLEAN
  4.2759 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2760 +    DECLARE
  4.2761 +      "issue_id_v" "issue"."id"%TYPE;
  4.2762 +    BEGIN
  4.2763 +      PERFORM "dont_require_transaction_isolation"();
  4.2764 +      LOCK TABLE "snapshot" IN EXCLUSIVE MODE;
  4.2765 +      UPDATE "area" SET "issue_quorum" = "view"."issue_quorum"
  4.2766 +        FROM "area_quorum" AS "view"
  4.2767 +        WHERE "area"."id" = "view"."area_id"
  4.2768 +        AND "area"."id" = "area_id_p";
  4.2769 +      SELECT "id" INTO "issue_id_v" FROM "issue_for_admission"
  4.2770 +        WHERE "area_id" = "area_id_p";
  4.2771 +      IF "issue_id_v" ISNULL THEN RETURN FALSE; END IF;
  4.2772 +      UPDATE "issue" SET
  4.2773 +        "admission_snapshot_id" = "latest_snapshot_id",
  4.2774 +        "state"                 = 'discussion',
  4.2775 +        "accepted"              = now(),
  4.2776 +        "phase_finished"        = NULL
  4.2777 +        WHERE "id" = "issue_id_v";
  4.2778 +      RETURN TRUE;
  4.2779 +    END;
  4.2780 +  $$;
  4.2781 +
  4.2782 +COMMENT ON FUNCTION "issue_admission"
  4.2783 +  ( "area"."id"%TYPE )
  4.2784 +  IS 'Checks if an issue in the area can be admitted for further discussion; returns TRUE on success in which case the function must be called again until it returns FALSE';
  4.2785 +
  4.2786 +
  4.2787 +CREATE OR REPLACE FUNCTION "check_issue"
  4.2788 +  ( "issue_id_p" "issue"."id"%TYPE,
  4.2789 +    "persist"    "check_issue_persistence" )
  4.2790 +  RETURNS "check_issue_persistence"
  4.2791 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.2792 +    DECLARE
  4.2793 +      "issue_row"         "issue"%ROWTYPE;
  4.2794 +      "last_calculated_v" "snapshot"."calculated"%TYPE;
  4.2795 +      "policy_row"        "policy"%ROWTYPE;
  4.2796 +      "initiative_row"    "initiative"%ROWTYPE;
  4.2797 +      "state_v"           "issue_state";
  4.2798 +    BEGIN
  4.2799 +      PERFORM "require_transaction_isolation"();
  4.2800 +      IF "persist" ISNULL THEN
  4.2801 +        SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p"
  4.2802 +          FOR UPDATE;
  4.2803 +        SELECT "calculated" INTO "last_calculated_v"
  4.2804 +          FROM "snapshot" JOIN "snapshot_issue"
  4.2805 +          ON "snapshot"."id" = "snapshot_issue"."snapshot_id"
  4.2806 +          WHERE "snapshot_issue"."issue_id" = "issue_id_p";
  4.2807 +        IF "issue_row"."closed" NOTNULL THEN
  4.2808 +          RETURN NULL;
  4.2809 +        END IF;
  4.2810 +        "persist"."state" := "issue_row"."state";
  4.2811 +        IF
  4.2812 +          ( "issue_row"."state" = 'admission' AND "last_calculated_v" >=
  4.2813 +            "issue_row"."created" + "issue_row"."max_admission_time" ) OR
  4.2814 +          ( "issue_row"."state" = 'discussion' AND now() >=
  4.2815 +            "issue_row"."accepted" + "issue_row"."discussion_time" ) OR
  4.2816 +          ( "issue_row"."state" = 'verification' AND now() >=
  4.2817 +            "issue_row"."half_frozen" + "issue_row"."verification_time" ) OR
  4.2818 +          ( "issue_row"."state" = 'voting' AND now() >=
  4.2819 +            "issue_row"."fully_frozen" + "issue_row"."voting_time" )
  4.2820 +        THEN
  4.2821 +          "persist"."phase_finished" := TRUE;
  4.2822 +        ELSE
  4.2823 +          "persist"."phase_finished" := FALSE;
  4.2824 +        END IF;
  4.2825 +        IF
  4.2826 +          NOT EXISTS (
  4.2827 +            -- all initiatives are revoked
  4.2828 +            SELECT NULL FROM "initiative"
  4.2829 +            WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
  4.2830 +          ) AND (
  4.2831 +            -- and issue has not been accepted yet
  4.2832 +            "persist"."state" = 'admission' OR
  4.2833 +            -- or verification time has elapsed
  4.2834 +            ( "persist"."state" = 'verification' AND
  4.2835 +              "persist"."phase_finished" ) OR
  4.2836 +            -- or no initiatives have been revoked lately
  4.2837 +            NOT EXISTS (
  4.2838 +              SELECT NULL FROM "initiative"
  4.2839 +              WHERE "issue_id" = "issue_id_p"
  4.2840 +              AND now() < "revoked" + "issue_row"."verification_time"
  4.2841 +            )
  4.2842 +          )
  4.2843 +        THEN
  4.2844 +          "persist"."issue_revoked" := TRUE;
  4.2845 +        ELSE
  4.2846 +          "persist"."issue_revoked" := FALSE;
  4.2847 +        END IF;
  4.2848 +        IF "persist"."phase_finished" OR "persist"."issue_revoked" THEN
  4.2849 +          UPDATE "issue" SET "phase_finished" = now()
  4.2850 +            WHERE "id" = "issue_row"."id";
  4.2851 +          RETURN "persist";
  4.2852 +        ELSIF
  4.2853 +          "persist"."state" IN ('admission', 'discussion', 'verification')
  4.2854 +        THEN
  4.2855 +          RETURN "persist";
  4.2856 +        ELSE
  4.2857 +          RETURN NULL;
  4.2858 +        END IF;
  4.2859 +      END IF;
  4.2860 +      IF
  4.2861 +        "persist"."state" IN ('admission', 'discussion', 'verification') AND
  4.2862 +        coalesce("persist"."snapshot_created", FALSE) = FALSE
  4.2863 +      THEN
  4.2864 +        IF "persist"."state" != 'admission' THEN
  4.2865 +          PERFORM "take_snapshot"("issue_id_p");
  4.2866 +          PERFORM "finish_snapshot"("issue_id_p");
  4.2867 +        END IF;
  4.2868 +        "persist"."snapshot_created" = TRUE;
  4.2869 +        IF "persist"."phase_finished" THEN
  4.2870 +          IF "persist"."state" = 'admission' THEN
  4.2871 +            UPDATE "issue" SET "admission_snapshot_id" = "latest_snapshot_id";
  4.2872 +          ELSIF "persist"."state" = 'discussion' THEN
  4.2873 +            UPDATE "issue" SET "half_freeze_snapshot_id" = "latest_snapshot_id";
  4.2874 +          ELSIF "persist"."state" = 'verification' THEN
  4.2875 +            UPDATE "issue" SET "full_freeze_snapshot_id" = "latest_snapshot_id";
  4.2876 +            SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
  4.2877 +            SELECT * INTO "policy_row" FROM "policy"
  4.2878 +              WHERE "id" = "issue_row"."policy_id";
  4.2879 +            FOR "initiative_row" IN
  4.2880 +              SELECT * FROM "initiative"
  4.2881 +              WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
  4.2882 +              FOR UPDATE
  4.2883 +            LOOP
  4.2884 +              IF
  4.2885 +                "initiative_row"."polling" OR (
  4.2886 +                  "initiative_row"."satisfied_supporter_count" > 
  4.2887 +                  "policy_row"."initiative_quorum" AND
  4.2888 +                  "initiative_row"."satisfied_supporter_count" *
  4.2889 +                  "policy_row"."initiative_quorum_den" >=
  4.2890 +                  "issue_row"."population" * "policy_row"."initiative_quorum_num"
  4.2891 +                )
  4.2892 +              THEN
  4.2893 +                UPDATE "initiative" SET "admitted" = TRUE
  4.2894 +                  WHERE "id" = "initiative_row"."id";
  4.2895 +              ELSE
  4.2896 +                UPDATE "initiative" SET "admitted" = FALSE
  4.2897 +                  WHERE "id" = "initiative_row"."id";
  4.2898 +              END IF;
  4.2899 +            END LOOP;
  4.2900 +          END IF;
  4.2901 +        END IF;
  4.2902 +        RETURN "persist";
  4.2903 +      END IF;
  4.2904 +      IF
  4.2905 +        "persist"."state" IN ('admission', 'discussion', 'verification') AND
  4.2906 +        coalesce("persist"."harmonic_weights_set", FALSE) = FALSE
  4.2907 +      THEN
  4.2908 +        PERFORM "set_harmonic_initiative_weights"("issue_id_p");
  4.2909 +        "persist"."harmonic_weights_set" = TRUE;
  4.2910 +        IF
  4.2911 +          "persist"."phase_finished" OR
  4.2912 +          "persist"."issue_revoked" OR
  4.2913 +          "persist"."state" = 'admission'
  4.2914 +        THEN
  4.2915 +          RETURN "persist";
  4.2916 +        ELSE
  4.2917 +          RETURN NULL;
  4.2918 +        END IF;
  4.2919 +      END IF;
  4.2920 +      IF "persist"."issue_revoked" THEN
  4.2921 +        IF "persist"."state" = 'admission' THEN
  4.2922 +          "state_v" := 'canceled_revoked_before_accepted';
  4.2923 +        ELSIF "persist"."state" = 'discussion' THEN
  4.2924 +          "state_v" := 'canceled_after_revocation_during_discussion';
  4.2925 +        ELSIF "persist"."state" = 'verification' THEN
  4.2926 +          "state_v" := 'canceled_after_revocation_during_verification';
  4.2927 +        END IF;
  4.2928 +        UPDATE "issue" SET
  4.2929 +          "state"          = "state_v",
  4.2930 +          "closed"         = "phase_finished",
  4.2931 +          "phase_finished" = NULL
  4.2932 +          WHERE "id" = "issue_id_p";
  4.2933 +        RETURN NULL;
  4.2934 +      END IF;
  4.2935 +      IF "persist"."state" = 'admission' THEN
  4.2936 +        SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p"
  4.2937 +          FOR UPDATE;
  4.2938 +        IF "issue_row"."phase_finished" NOTNULL THEN
  4.2939 +          UPDATE "issue" SET
  4.2940 +            "state"          = 'canceled_issue_not_accepted',
  4.2941 +            "closed"         = "phase_finished",
  4.2942 +            "phase_finished" = NULL
  4.2943 +            WHERE "id" = "issue_id_p";
  4.2944 +        END IF;
  4.2945 +        RETURN NULL;
  4.2946 +      END IF;
  4.2947 +      IF "persist"."phase_finished" THEN
  4.2948 +        IF "persist"."state" = 'discussion' THEN
  4.2949 +          UPDATE "issue" SET
  4.2950 +            "state"          = 'verification',
  4.2951 +            "half_frozen"    = "phase_finished",
  4.2952 +            "phase_finished" = NULL
  4.2953 +            WHERE "id" = "issue_id_p";
  4.2954 +          RETURN NULL;
  4.2955 +        END IF;
  4.2956 +        IF "persist"."state" = 'verification' THEN
  4.2957 +          SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p"
  4.2958 +            FOR UPDATE;
  4.2959 +          SELECT * INTO "policy_row" FROM "policy"
  4.2960 +            WHERE "id" = "issue_row"."policy_id";
  4.2961 +          IF EXISTS (
  4.2962 +            SELECT NULL FROM "initiative"
  4.2963 +            WHERE "issue_id" = "issue_id_p" AND "admitted" = TRUE
  4.2964 +          ) THEN
  4.2965 +            UPDATE "issue" SET
  4.2966 +              "state"          = 'voting',
  4.2967 +              "fully_frozen"   = "phase_finished",
  4.2968 +              "phase_finished" = NULL
  4.2969 +              WHERE "id" = "issue_id_p";
  4.2970 +          ELSE
  4.2971 +            UPDATE "issue" SET
  4.2972 +              "state"          = 'canceled_no_initiative_admitted',
  4.2973 +              "fully_frozen"   = "phase_finished",
  4.2974 +              "closed"         = "phase_finished",
  4.2975 +              "phase_finished" = NULL
  4.2976 +              WHERE "id" = "issue_id_p";
  4.2977 +            -- NOTE: The following DELETE statements have effect only when
  4.2978 +            --       issue state has been manipulated
  4.2979 +            DELETE FROM "direct_voter"     WHERE "issue_id" = "issue_id_p";
  4.2980 +            DELETE FROM "delegating_voter" WHERE "issue_id" = "issue_id_p";
  4.2981 +            DELETE FROM "battle"           WHERE "issue_id" = "issue_id_p";
  4.2982 +          END IF;
  4.2983 +          RETURN NULL;
  4.2984 +        END IF;
  4.2985 +        IF "persist"."state" = 'voting' THEN
  4.2986 +          IF coalesce("persist"."closed_voting", FALSE) = FALSE THEN
  4.2987 +            PERFORM "close_voting"("issue_id_p");
  4.2988 +            "persist"."closed_voting" = TRUE;
  4.2989 +            RETURN "persist";
  4.2990 +          END IF;
  4.2991 +          PERFORM "calculate_ranks"("issue_id_p");
  4.2992 +          RETURN NULL;
  4.2993 +        END IF;
  4.2994 +      END IF;
  4.2995 +      RAISE WARNING 'should not happen';
  4.2996 +      RETURN NULL;
  4.2997 +    END;
  4.2998 +  $$;
  4.2999 +
  4.3000 +
  4.3001 +CREATE OR REPLACE FUNCTION "check_everything"()
  4.3002 +  RETURNS VOID
  4.3003 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.3004 +    DECLARE
  4.3005 +      "area_id_v"     "area"."id"%TYPE;
  4.3006 +      "snapshot_id_v" "snapshot"."id"%TYPE;
  4.3007 +      "issue_id_v"    "issue"."id"%TYPE;
  4.3008 +      "persist_v"     "check_issue_persistence";
  4.3009 +    BEGIN
  4.3010 +      RAISE WARNING 'Function "check_everything" should only be used for development and debugging purposes';
  4.3011 +      DELETE FROM "expired_session";
  4.3012 +      DELETE FROM "expired_token";
  4.3013 +      DELETE FROM "expired_snapshot";
  4.3014 +      PERFORM "check_activity"();
  4.3015 +      PERFORM "calculate_member_counts"();
  4.3016 +      FOR "area_id_v" IN SELECT "id" FROM "area_with_unaccepted_issues" LOOP
  4.3017 +        SELECT "take_snapshot"(NULL, "area_id_v") INTO "snapshot_id_v";
  4.3018 +        PERFORM "finish_snapshot"("issue_id") FROM "snapshot_issue"
  4.3019 +          WHERE "snapshot_id" = "snapshot_id_v";
  4.3020 +        LOOP
  4.3021 +          EXIT WHEN "issue_admission"("area_id_v") = FALSE;
  4.3022 +        END LOOP;
  4.3023 +      END LOOP;
  4.3024 +      FOR "issue_id_v" IN SELECT "id" FROM "open_issue" LOOP
  4.3025 +        "persist_v" := NULL;
  4.3026 +        LOOP
  4.3027 +          "persist_v" := "check_issue"("issue_id_v", "persist_v");
  4.3028 +          EXIT WHEN "persist_v" ISNULL;
  4.3029 +        END LOOP;
  4.3030 +      END LOOP;
  4.3031 +      RETURN;
  4.3032 +    END;
  4.3033 +  $$;
  4.3034 +
  4.3035 +COMMENT ON FUNCTION "check_everything"() IS 'Amongst other regular tasks, this function performs "check_issue" for every open issue. Use this function only for development and debugging purposes, as you may run into locking and/or serialization problems in productive environments. For production, use lf_update binary instead';
  4.3036 +
  4.3037 +
  4.3038 +CREATE OR REPLACE FUNCTION "clean_issue"("issue_id_p" "issue"."id"%TYPE)
  4.3039 +  RETURNS VOID
  4.3040 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.3041 +    BEGIN
  4.3042 +      IF EXISTS (
  4.3043 +        SELECT NULL FROM "issue" WHERE "id" = "issue_id_p" AND "cleaned" ISNULL
  4.3044 +      ) THEN
  4.3045 +        -- override protection triggers:
  4.3046 +        INSERT INTO "temporary_transaction_data" ("key", "value")
  4.3047 +          VALUES ('override_protection_triggers', TRUE::TEXT);
  4.3048 +        -- clean data:
  4.3049 +        DELETE FROM "delegating_voter"
  4.3050 +          WHERE "issue_id" = "issue_id_p";
  4.3051 +        DELETE FROM "direct_voter"
  4.3052 +          WHERE "issue_id" = "issue_id_p";
  4.3053 +        DELETE FROM "delegating_interest_snapshot"
  4.3054 +          WHERE "issue_id" = "issue_id_p";
  4.3055 +        DELETE FROM "direct_interest_snapshot"
  4.3056 +          WHERE "issue_id" = "issue_id_p";
  4.3057 +        DELETE FROM "non_voter"
  4.3058 +          WHERE "issue_id" = "issue_id_p";
  4.3059 +        DELETE FROM "delegation"
  4.3060 +          WHERE "issue_id" = "issue_id_p";
  4.3061 +        DELETE FROM "supporter"
  4.3062 +          USING "initiative"  -- NOTE: due to missing index on issue_id
  4.3063 +          WHERE "initiative"."issue_id" = "issue_id_p"
  4.3064 +          AND "supporter"."initiative_id" = "initiative_id";
  4.3065 +        -- mark issue as cleaned:
  4.3066 +        UPDATE "issue" SET "cleaned" = now() WHERE "id" = "issue_id_p";
  4.3067 +        -- finish overriding protection triggers (avoids garbage):
  4.3068 +        DELETE FROM "temporary_transaction_data"
  4.3069 +          WHERE "key" = 'override_protection_triggers';
  4.3070 +      END IF;
  4.3071 +      RETURN;
  4.3072 +    END;
  4.3073 +  $$;
  4.3074 +
  4.3075 +
  4.3076 +CREATE OR REPLACE FUNCTION "delete_member"("member_id_p" "member"."id"%TYPE)
  4.3077 +  RETURNS VOID
  4.3078 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.3079 +    BEGIN
  4.3080 +      UPDATE "member" SET
  4.3081 +        "last_login"                   = NULL,
  4.3082 +        "last_delegation_check"        = NULL,
  4.3083 +        "login"                        = NULL,
  4.3084 +        "password"                     = NULL,
  4.3085 +        "authority"                    = NULL,
  4.3086 +        "authority_uid"                = NULL,
  4.3087 +        "authority_login"              = NULL,
  4.3088 +        "locked"                       = TRUE,
  4.3089 +        "active"                       = FALSE,
  4.3090 +        "notify_email"                 = NULL,
  4.3091 +        "notify_email_unconfirmed"     = NULL,
  4.3092 +        "notify_email_secret"          = NULL,
  4.3093 +        "notify_email_secret_expiry"   = NULL,
  4.3094 +        "notify_email_lock_expiry"     = NULL,
  4.3095 +        "disable_notifications"        = TRUE,
  4.3096 +        "notification_counter"         = DEFAULT,
  4.3097 +        "notification_sample_size"     = 0,
  4.3098 +        "notification_dow"             = NULL,
  4.3099 +        "notification_hour"            = NULL,
  4.3100 +        "login_recovery_expiry"        = NULL,
  4.3101 +        "password_reset_secret"        = NULL,
  4.3102 +        "password_reset_secret_expiry" = NULL,
  4.3103 +        "location"                     = NULL
  4.3104 +        WHERE "id" = "member_id_p";
  4.3105 +      -- "text_search_data" is updated by triggers
  4.3106 +      DELETE FROM "setting"            WHERE "member_id" = "member_id_p";
  4.3107 +      DELETE FROM "setting_map"        WHERE "member_id" = "member_id_p";
  4.3108 +      DELETE FROM "member_relation_setting" WHERE "member_id" = "member_id_p";
  4.3109 +      DELETE FROM "member_image"       WHERE "member_id" = "member_id_p";
  4.3110 +      DELETE FROM "contact"            WHERE "member_id" = "member_id_p";
  4.3111 +      DELETE FROM "ignored_member"     WHERE "member_id" = "member_id_p";
  4.3112 +      DELETE FROM "session"            WHERE "member_id" = "member_id_p";
  4.3113 +      DELETE FROM "area_setting"       WHERE "member_id" = "member_id_p";
  4.3114 +      DELETE FROM "issue_setting"      WHERE "member_id" = "member_id_p";
  4.3115 +      DELETE FROM "ignored_initiative" WHERE "member_id" = "member_id_p";
  4.3116 +      DELETE FROM "initiative_setting" WHERE "member_id" = "member_id_p";
  4.3117 +      DELETE FROM "suggestion_setting" WHERE "member_id" = "member_id_p";
  4.3118 +      DELETE FROM "delegation"         WHERE "truster_id" = "member_id_p";
  4.3119 +      DELETE FROM "non_voter"          WHERE "member_id" = "member_id_p";
  4.3120 +      DELETE FROM "direct_voter" USING "issue"
  4.3121 +        WHERE "direct_voter"."issue_id" = "issue"."id"
  4.3122 +        AND "issue"."closed" ISNULL
  4.3123 +        AND "member_id" = "member_id_p";
  4.3124 +      RETURN;
  4.3125 +    END;
  4.3126 +  $$;
  4.3127 +
  4.3128 +
  4.3129 +CREATE OR REPLACE FUNCTION "delete_private_data"()
  4.3130 +  RETURNS VOID
  4.3131 +  LANGUAGE 'plpgsql' VOLATILE AS $$
  4.3132 +    BEGIN
  4.3133 +      DELETE FROM "temporary_transaction_data";
  4.3134 +      DELETE FROM "member" WHERE "activated" ISNULL;
  4.3135 +      UPDATE "member" SET
  4.3136 +        "invite_code"                  = NULL,
  4.3137 +        "invite_code_expiry"           = NULL,
  4.3138 +        "admin_comment"                = NULL,
  4.3139 +        "last_login"                   = NULL,
  4.3140 +        "last_delegation_check"        = NULL,
  4.3141 +        "login"                        = NULL,
  4.3142 +        "password"                     = NULL,
  4.3143 +        "authority"                    = NULL,
  4.3144 +        "authority_uid"                = NULL,
  4.3145 +        "authority_login"              = NULL,
  4.3146 +        "lang"                         = NULL,
  4.3147 +        "notify_email"                 = NULL,
  4.3148 +        "notify_email_unconfirmed"     = NULL,
  4.3149 +        "notify_email_secret"          = NULL,
  4.3150 +        "notify_email_secret_expiry"   = NULL,
  4.3151 +        "notify_email_lock_expiry"     = NULL,
  4.3152 +        "disable_notifications"        = TRUE,
  4.3153 +        "notification_counter"         = DEFAULT,
  4.3154 +        "notification_sample_size"     = 0,
  4.3155 +        "notification_dow"             = NULL,
  4.3156 +        "notification_hour"            = NULL,
  4.3157 +        "login_recovery_expiry"        = NULL,
  4.3158 +        "password_reset_secret"        = NULL,
  4.3159 +        "password_reset_secret_expiry" = NULL,
  4.3160 +        "location"                     = NULL;
  4.3161 +      -- "text_search_data" is updated by triggers
  4.3162 +      DELETE FROM "setting";
  4.3163 +      DELETE FROM "setting_map";
  4.3164 +      DELETE FROM "member_relation_setting";
  4.3165 +      DELETE FROM "member_image";
  4.3166 +      DELETE FROM "contact";
  4.3167 +      DELETE FROM "ignored_member";
  4.3168 +      DELETE FROM "session";
  4.3169 +      DELETE FROM "area_setting";
  4.3170 +      DELETE FROM "issue_setting";
  4.3171 +      DELETE FROM "ignored_initiative";
  4.3172 +      DELETE FROM "initiative_setting";
  4.3173 +      DELETE FROM "suggestion_setting";
  4.3174 +      DELETE FROM "non_voter";
  4.3175 +      DELETE FROM "direct_voter" USING "issue"
  4.3176 +        WHERE "direct_voter"."issue_id" = "issue"."id"
  4.3177 +        AND "issue"."closed" ISNULL;
  4.3178 +      RETURN;
  4.3179 +    END;
  4.3180 +  $$;
  4.3181 +
  4.3182 +
  4.3183 +CREATE TEMPORARY TABLE "old_snapshot" AS
  4.3184 +  SELECT "ordered".*, row_number() OVER () AS "snapshot_id"
  4.3185 +  FROM (
  4.3186 +    SELECT * FROM (
  4.3187 +      SELECT
  4.3188 +        "id" AS "issue_id",
  4.3189 +        'end_of_admission'::"snapshot_event" AS "event",
  4.3190 +        "accepted" AS "calculated"
  4.3191 +      FROM "issue" WHERE "accepted" NOTNULL
  4.3192 +      UNION ALL
  4.3193 +      SELECT
  4.3194 +        "id" AS "issue_id",
  4.3195 +        'half_freeze'::"snapshot_event" AS "event",
  4.3196 +        "half_frozen" AS "calculated"
  4.3197 +      FROM "issue" WHERE "half_frozen" NOTNULL
  4.3198 +      UNION ALL
  4.3199 +      SELECT
  4.3200 +        "id" AS "issue_id",
  4.3201 +        'full_freeze'::"snapshot_event" AS "event",
  4.3202 +        "fully_frozen" AS "calculated"
  4.3203 +      FROM "issue" WHERE "fully_frozen" NOTNULL
  4.3204 +    ) AS "unordered"
  4.3205 +    ORDER BY "calculated", "issue_id", "event"
  4.3206 +  ) AS "ordered";
  4.3207 +
  4.3208 +
  4.3209 +INSERT INTO "snapshot" ("id", "calculated", "population", "area_id", "issue_id")
  4.3210 +  SELECT
  4.3211 +    "old_snapshot"."snapshot_id" AS "id",
  4.3212 +    "old_snapshot"."calculated",
  4.3213 +    ( SELECT COALESCE(sum("weight"), 0)
  4.3214 +      FROM "direct_population_snapshot" "dps"
  4.3215 +      WHERE "dps"."issue_id" = "old_snapshot"."issue_id"
  4.3216 +      AND   "dps"."event"    = "old_snapshot"."event"
  4.3217 +    ) AS "population",
  4.3218 +    "issue"."area_id" AS "area_id",
  4.3219 +    "issue"."id" AS "issue_id"
  4.3220 +  FROM "old_snapshot" JOIN "issue"
  4.3221 +  ON "old_snapshot"."issue_id" = "issue"."id";
  4.3222 +
  4.3223 +
  4.3224 +INSERT INTO "snapshot_issue" ("snapshot_id", "issue_id")
  4.3225 +  SELECT "id" AS "snapshot_id", "issue_id" FROM "snapshot";
  4.3226 +
  4.3227 +
  4.3228 +INSERT INTO "snapshot_population" ("snapshot_id", "member_id")
  4.3229 +  SELECT
  4.3230 +    "old_snapshot"."snapshot_id",
  4.3231 +    "direct_population_snapshot"."member_id"
  4.3232 +  FROM "old_snapshot" JOIN "direct_population_snapshot"
  4.3233 +  ON "old_snapshot"."issue_id" = "direct_population_snapshot"."issue_id"
  4.3234 +  AND "old_snapshot"."event" = "direct_population_snapshot"."event";
  4.3235 +
  4.3236 +INSERT INTO "snapshot_population" ("snapshot_id", "member_id")
  4.3237 +  SELECT
  4.3238 +    "old_snapshot"."snapshot_id",
  4.3239 +    "delegating_population_snapshot"."member_id"
  4.3240 +  FROM "old_snapshot" JOIN "delegating_population_snapshot"
  4.3241 +  ON "old_snapshot"."issue_id" = "delegating_population_snapshot"."issue_id"
  4.3242 +  AND "old_snapshot"."event" = "delegating_population_snapshot"."event";
  4.3243 +
  4.3244 +
  4.3245 +INSERT INTO "direct_interest_snapshot"
  4.3246 +  ("snapshot_id", "issue_id", "member_id", "weight")
  4.3247 +  SELECT
  4.3248 +    "old_snapshot"."snapshot_id",
  4.3249 +    "old_snapshot"."issue_id",
  4.3250 +    "direct_interest_snapshot_old"."member_id",
  4.3251 +    "direct_interest_snapshot_old"."weight"
  4.3252 +  FROM "old_snapshot" JOIN "direct_interest_snapshot_old"
  4.3253 +  ON "old_snapshot"."issue_id" = "direct_interest_snapshot_old"."issue_id"
  4.3254 +  AND "old_snapshot"."event" = "direct_interest_snapshot_old"."event";
  4.3255 +
  4.3256 +INSERT INTO "delegating_interest_snapshot"
  4.3257 +  ( "snapshot_id", "issue_id",
  4.3258 +    "member_id", "weight", "scope", "delegate_member_ids" )
  4.3259 +  SELECT
  4.3260 +    "old_snapshot"."snapshot_id",
  4.3261 +    "old_snapshot"."issue_id",
  4.3262 +    "delegating_interest_snapshot_old"."member_id",
  4.3263 +    "delegating_interest_snapshot_old"."weight",
  4.3264 +    "delegating_interest_snapshot_old"."scope",
  4.3265 +    "delegating_interest_snapshot_old"."delegate_member_ids"
  4.3266 +  FROM "old_snapshot" JOIN "delegating_interest_snapshot_old"
  4.3267 +  ON "old_snapshot"."issue_id" = "delegating_interest_snapshot_old"."issue_id"
  4.3268 +  AND "old_snapshot"."event" = "delegating_interest_snapshot_old"."event";
  4.3269 +
  4.3270 +INSERT INTO "direct_supporter_snapshot"
  4.3271 +  ( "snapshot_id", "issue_id",
  4.3272 +    "initiative_id", "member_id", "draft_id", "informed", "satisfied" )
  4.3273 +  SELECT
  4.3274 +    "old_snapshot"."snapshot_id",
  4.3275 +    "old_snapshot"."issue_id",
  4.3276 +    "direct_supporter_snapshot_old"."initiative_id",
  4.3277 +    "direct_supporter_snapshot_old"."member_id",
  4.3278 +    "direct_supporter_snapshot_old"."draft_id",
  4.3279 +    "direct_supporter_snapshot_old"."informed",
  4.3280 +    "direct_supporter_snapshot_old"."satisfied"
  4.3281 +  FROM "old_snapshot" JOIN "direct_supporter_snapshot_old"
  4.3282 +  ON "old_snapshot"."issue_id" = "direct_supporter_snapshot_old"."issue_id"
  4.3283 +  AND "old_snapshot"."event" = "direct_supporter_snapshot_old"."event";
  4.3284 +
  4.3285 +
  4.3286 +ALTER TABLE "issue" DISABLE TRIGGER USER;  -- NOTE: required to modify table later
  4.3287 +
  4.3288 +UPDATE "issue" SET "latest_snapshot_id" = "snapshot"."id"
  4.3289 +  FROM (
  4.3290 +    SELECT DISTINCT ON ("issue_id") "issue_id", "id"
  4.3291 +    FROM "snapshot" ORDER BY "issue_id", "id" DESC
  4.3292 +  ) AS "snapshot"
  4.3293 +  WHERE "snapshot"."issue_id" = "issue"."id";
  4.3294 +
  4.3295 +UPDATE "issue" SET "admission_snapshot_id" = "old_snapshot"."snapshot_id"
  4.3296 +  FROM "old_snapshot"
  4.3297 +  WHERE "old_snapshot"."issue_id" = "issue"."id"
  4.3298 +  AND "old_snapshot"."event" = 'end_of_admission';
  4.3299 +
  4.3300 +UPDATE "issue" SET "half_freeze_snapshot_id" = "old_snapshot"."snapshot_id"
  4.3301 +  FROM "old_snapshot"
  4.3302 +  WHERE "old_snapshot"."issue_id" = "issue"."id"
  4.3303 +  AND "old_snapshot"."event" = 'half_freeze';
  4.3304 +
  4.3305 +UPDATE "issue" SET "full_freeze_snapshot_id" = "old_snapshot"."snapshot_id"
  4.3306 +  FROM "old_snapshot"
  4.3307 +  WHERE "old_snapshot"."issue_id" = "issue"."id"
  4.3308 +  AND "old_snapshot"."event" = 'full_freeze';
  4.3309 +
  4.3310 +ALTER TABLE "issue" ENABLE TRIGGER USER;
  4.3311 +
  4.3312 +
  4.3313 +DROP TABLE "old_snapshot";
  4.3314 +
  4.3315 +DROP TABLE "direct_supporter_snapshot_old";
  4.3316 +DROP TABLE "delegating_interest_snapshot_old";
  4.3317 +DROP TABLE "direct_interest_snapshot_old";
  4.3318 +DROP TABLE "delegating_population_snapshot";
  4.3319 +DROP TABLE "direct_population_snapshot";
  4.3320 +
  4.3321 +
  4.3322 +DROP VIEW "open_issue";
  4.3323 +
  4.3324 +
  4.3325 +ALTER TABLE "issue" DROP COLUMN "latest_snapshot_event";
  4.3326 +
  4.3327 +
  4.3328 +CREATE VIEW "open_issue" AS
  4.3329 +  SELECT * FROM "issue" WHERE "closed" ISNULL;
  4.3330 +
  4.3331 +COMMENT ON VIEW "open_issue" IS 'All open issues';
  4.3332 +
  4.3333 +
  4.3334 +-- NOTE: create "issue_for_admission" view after altering table "issue"
  4.3335 +CREATE VIEW "issue_for_admission" AS
  4.3336 +  SELECT DISTINCT ON ("issue"."area_id")
  4.3337 +    "issue".*,
  4.3338 +    max("initiative"."supporter_count") AS "max_supporter_count"
  4.3339 +  FROM "issue"
  4.3340 +  JOIN "policy" ON "issue"."policy_id" = "policy"."id"
  4.3341 +  JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
  4.3342 +  JOIN "area" ON "issue"."area_id" = "area"."id"
  4.3343 +  WHERE "issue"."state" = 'admission'::"issue_state"
  4.3344 +  AND now() >= "issue"."created" + "issue"."min_admission_time"
  4.3345 +  AND "initiative"."supporter_count" >= "policy"."issue_quorum"
  4.3346 +  AND "initiative"."supporter_count" * "policy"."issue_quorum_den" >=
  4.3347 +      "issue"."population" * "policy"."issue_quorum_num"
  4.3348 +  AND "initiative"."supporter_count" >= "area"."issue_quorum"
  4.3349 +  AND "initiative"."revoked" ISNULL
  4.3350 +  GROUP BY "issue"."id"
  4.3351 +  ORDER BY "issue"."area_id", "max_supporter_count" DESC, "issue"."id";
  4.3352 +
  4.3353 +COMMENT ON VIEW "issue_for_admission" IS 'Contains up to 1 issue per area eligible to pass from ''admission'' to ''discussion'' state; needs to be recalculated after admitting the issue in this view';
  4.3354 +
  4.3355 +
  4.3356 +DROP TYPE "snapshot_event";
  4.3357 +
  4.3358 +
  4.3359 +ALTER TABLE "issue" ADD CONSTRAINT "snapshot_required" CHECK (
  4.3360 +  ("half_frozen" ISNULL OR "half_freeze_snapshot_id" NOTNULL) AND
  4.3361 +  ("fully_frozen" ISNULL OR "full_freeze_snapshot_id" NOTNULL) );
  4.3362 +
  4.3363 +
  4.3364 +COMMIT;

Impressum / About Us