# HG changeset patch # User jbe # Date 1581069157 -3600 # Node ID 9570aeabf4fc75a1270aaee7a868e4d0a2a5c3e7 # Parent aa0620c9c4dfe5753731cea9a2a4cc226b6903b6 Added update script to version 4.2.0 diff -r aa0620c9c4df -r 9570aeabf4fc update/core-update.v4.1.0-v4.2.0.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/update/core-update.v4.1.0-v4.2.0.sql Fri Feb 07 10:52:37 2020 +0100 @@ -0,0 +1,1208 @@ +BEGIN; + +CREATE OR REPLACE VIEW "liquid_feedback_version" AS + SELECT * FROM (VALUES ('4.2.0-incomplete-update', 4, 2, -1)) + AS "subquery"("string", "major", "minor", "revision"); + +ALTER TYPE "event_type" ADD VALUE IF NOT EXISTS 'posting_created'; + +COMMIT; + +BEGIN; + +CREATE OR REPLACE VIEW "liquid_feedback_version" AS + SELECT * FROM (VALUES ('4.2.0', 4, 2, 0)) + AS "subquery"("string", "major", "minor", "revision"); + +DROP VIEW "newsletter_to_send"; +DROP VIEW "scheduled_notification_to_send"; +DROP VIEW "member_contingent_left"; +DROP VIEW "member_contingent"; +DROP VIEW "expired_snapshot"; +DROP VIEW "current_draft"; +DROP VIEW "opening_draft"; +DROP VIEW "area_with_unaccepted_issues"; +DROP VIEW "member_to_notify"; +DROP VIEW "member_eligible_to_be_notified"; + +CREATE EXTENSION IF NOT EXISTS btree_gist; +CREATE EXTENSION IF NOT EXISTS conflux; + +DROP FUNCTION "text_search_query" (TEXT); + +ALTER TABLE "system_setting" DROP COLUMN "snapshot_retention"; + +CREATE TABLE "file" ( + "id" SERIAL8 PRIMARY KEY, + UNIQUE ("content_type", "hash"), + "content_type" TEXT NOT NULL, + "hash" TEXT NOT NULL, + "data" BYTEA NOT NULL, + "preview_content_type" TEXT, + "preview_data" BYTEA ); + +COMMENT ON TABLE "file" IS 'Table holding file contents for draft attachments'; + +COMMENT ON COLUMN "file"."content_type" IS 'Content type of "data"'; +COMMENT ON COLUMN "file"."hash" IS 'Hash of "data" to avoid storing duplicates where content-type and data is identical'; +COMMENT ON COLUMN "file"."data" IS 'Binary content'; +COMMENT ON COLUMN "file"."preview_content_type" IS 'Content type of "preview_data"'; +COMMENT ON COLUMN "file"."preview_data" IS 'Preview (e.g. preview image)'; + +ALTER TABLE "member" DROP COLUMN "text_search_data"; +DROP TRIGGER "update_text_search_data" ON "member"; + +CREATE INDEX "member_useterms_member_id_contract_identifier" ON "member_useterms" ("member_id", "contract_identifier"); + +ALTER TABLE "member_profile" DROP COLUMN "text_search_data"; +DROP TRIGGER "update_text_search_data" ON "member_profile"; + +ALTER TABLE "contact" ADD COLUMN "following" BOOLEAN NOT NULL DEFAULT TRUE; + +COMMENT ON COLUMN "contact"."following" IS 'TRUE = actions of contact are shown in personal timeline'; + +ALTER TABLE "unit" DROP COLUMN "text_search_data"; +DROP TRIGGER "update_text_search_data" ON "unit"; + +ALTER TABLE "area" DROP COLUMN "text_search_data"; +DROP TRIGGER "update_text_search_data" ON "area"; + +DROP INDEX "issue_accepted_idx"; +DROP INDEX "issue_half_frozen_idx"; +DROP INDEX "issue_fully_frozen_idx"; +ALTER INDEX "issue_created_idx_open" RENAME TO "issue_open_created_idx"; +DROP INDEX "issue_closed_idx_canceled"; +ALTER INDEX "issue_latest_snapshot_id" RENAME TO "issue_latest_snapshot_id_idx"; +ALTER INDEX "issue_admission_snapshot_id" RENAME TO "issue_admission_snapshot_id_idx"; +ALTER INDEX "issue_half_freeze_snapshot_id" RENAME TO "issue_half_freeze_snapshot_id_idx"; +ALTER INDEX "issue_full_freeze_snapshot_id" RENAME TO "issue_full_freeze_snapshot_id_idx"; + +ALTER TABLE "initiative" ADD COLUMN "content" TEXT; +ALTER TABLE "initiative" DROP COLUMN "text_search_data"; +ALTER TABLE "initiative" DROP COLUMN "draft_text_search_data"; +DROP INDEX "initiative_revoked_idx"; +DROP TRIGGER "update_text_search_data" ON "initiative"; + +COMMENT ON COLUMN "initiative"."content" IS 'Initiative text (automatically copied from most recent draft)'; + +ALTER TABLE "battle" DROP CONSTRAINT "initiative_ids_not_equal"; +ALTER TABLE "battle" ADD CONSTRAINT "initiative_ids_not_equal" CHECK ( + "winning_initiative_id" != "losing_initiative_id" AND + ("winning_initiative_id" NOTNULL OR "losing_initiative_id" NOTNULL) ); + +ALTER TABLE "draft" DROP COLUMN "text_search_data"; +DROP TRIGGER "update_text_search_data" ON "draft"; + +CREATE TABLE "draft_attachment" ( + "id" SERIAL8 PRIMARY KEY, + "draft_id" INT8 REFERENCES "draft" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + "file_id" INT8 REFERENCES "file" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + "content_type" TEXT, + "title" TEXT, + "description" TEXT ); + +COMMENT ON TABLE "draft_attachment" IS 'Binary attachments for a draft (images, PDF file, etc.); Implicitly ordered through ''id'' column'; + +ALTER TABLE "suggestion" DROP COLUMN "text_search_data"; +DROP TRIGGER "update_text_search_data" ON "suggestion"; + +ALTER TABLE "direct_voter" DROP COLUMN "text_search_data"; +DROP TRIGGER "update_text_search_data" ON "direct_voter"; + +CREATE TABLE "posting" ( + UNIQUE ("author_id", "id"), -- index needed for foreign-key on table "posting_lexeme" + "id" SERIAL8 PRIMARY KEY, + "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + "created" TIMESTAMPTZ NOT NULL DEFAULT now(), + "message" TEXT NOT NULL, + "unit_id" INT4 REFERENCES "unit" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + "area_id" INT4, + FOREIGN KEY ("unit_id", "area_id") REFERENCES "area" ("unit_id", "id") ON DELETE CASCADE ON UPDATE CASCADE, + "policy_id" INT4 REFERENCES "policy" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("area_id", "issue_id") REFERENCES "issue" ("area_id", "id") ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY ("policy_id", "issue_id") REFERENCES "issue" ("policy_id", "id") ON DELETE CASCADE ON UPDATE CASCADE, + "initiative_id" INT4, + "suggestion_id" INT8, + -- NOTE: no referential integrity for suggestions because those are + -- actually deleted + -- FOREIGN KEY ("initiative_id", "suggestion_id") + -- REFERENCES "suggestion" ("initiative_id", "id") + -- ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "area_requires_unit" CHECK ( + "area_id" ISNULL OR "unit_id" NOTNULL ), + CONSTRAINT "policy_set_when_issue_set" CHECK ( + ("policy_id" NOTNULL) = ("issue_id" NOTNULL) ), + CONSTRAINT "issue_requires_area" CHECK ( + "issue_id" ISNULL OR "area_id" NOTNULL ), + CONSTRAINT "initiative_requires_issue" CHECK ( + "initiative_id" ISNULL OR "issue_id" NOTNULL ), + CONSTRAINT "suggestion_requires_initiative" CHECK ( + "suggestion_id" ISNULL OR "initiative_id" NOTNULL ) ); +CREATE INDEX "posting_global_idx" ON "posting" USING gist ((pstamp("author_id", "id"))); +CREATE INDEX "posting_unit_idx" ON "posting" USING gist ("unit_id", (pstamp("author_id", "id"))) WHERE "unit_id" NOTNULL; +CREATE INDEX "posting_area_idx" ON "posting" USING gist ("area_id", (pstamp("author_id", "id"))) WHERE "area_id" NOTNULL; +CREATE INDEX "posting_policy_idx" ON "posting" USING gist ("policy_id", (pstamp("author_id", "id"))) WHERE "policy_id" NOTNULL; +CREATE INDEX "posting_issue_idx" ON "posting" USING gist ("issue_id", (pstamp("author_id", "id"))) WHERE "issue_id" NOTNULL; +CREATE INDEX "posting_initiative_idx" ON "posting" USING gist ("initiative_id", (pstamp("author_id", "id"))) WHERE "initiative_id" NOTNULL; +CREATE INDEX "posting_suggestion_idx" ON "posting" USING gist ("suggestion_id", (pstamp("author_id", "id"))) WHERE "suggestion_id" NOTNULL; +COMMENT ON TABLE "posting" IS 'Text postings of members; a text posting may optionally be associated to a unit, area, policy, issue, initiative, or suggestion'; + +CREATE TABLE "posting_lexeme" ( + PRIMARY KEY ("posting_id", "lexeme"), + FOREIGN KEY ("posting_id", "author_id") REFERENCES "posting" ("id", "author_id") ON DELETE CASCADE ON UPDATE CASCADE, + "posting_id" INT8, + "lexeme" TEXT, + "author_id" INT4 ); +CREATE INDEX "posting_lexeme_idx" ON "posting_lexeme" USING gist ("lexeme", (pstamp("author_id", "posting_id"))); + +COMMENT ON TABLE "posting_lexeme" IS 'Helper table to allow searches for hashtags.'; + +ALTER TABLE "event" ADD COLUMN "posting_id" INT8 REFERENCES "posting" ("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_issue_state_changed"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_initiative_creation_or_revocation_or_new_draft"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_suggestion_creation"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_suggestion_removal"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_value_less_member_event"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_member_active"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_member_name_updated"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_interest"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_initiator"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_support"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_support_updated"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_suggestion_rated"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_delegation"; +ALTER TABLE "event" DROP CONSTRAINT "constr_for_contact"; +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_issue_state_changed" CHECK ( + "event" != 'issue_state_changed' OR ( + "posting_id" ISNULL AND + "member_id" ISNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" ISNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_initiative_creation_or_revocation_or_new_draft" CHECK ( + "event" NOT IN ( + 'initiative_created_in_new_issue', + 'initiative_created_in_existing_issue', + 'initiative_revoked', + 'new_draft_created' + ) OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" NOTNULL AND + "draft_id" NOTNULL AND + "suggestion_id" ISNULL AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_suggestion_creation" CHECK ( + "event" != 'suggestion_created' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" NOTNULL AND + "draft_id" ISNULL AND + "suggestion_id" NOTNULL AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_suggestion_removal" CHECK ( + "event" != 'suggestion_deleted' OR ( + "posting_id" ISNULL AND + "member_id" ISNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" NOTNULL AND + "draft_id" ISNULL AND + "suggestion_id" NOTNULL AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_value_less_member_event" CHECK ( + "event" NOT IN ( + 'member_activated', + 'member_deleted', + 'member_profile_updated', + 'member_image_updated' + ) OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" ISNULL AND + "area_id" ISNULL AND + "policy_id" ISNULL AND + "issue_id" ISNULL AND + "state" ISNULL AND + "initiative_id" ISNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_member_active" CHECK ( + "event" != 'member_active' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" ISNULL AND + "area_id" ISNULL AND + "policy_id" ISNULL AND + "issue_id" ISNULL AND + "state" ISNULL AND + "initiative_id" ISNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" NOTNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_member_name_updated" CHECK ( + "event" != 'member_name_updated' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" ISNULL AND + "area_id" ISNULL AND + "policy_id" ISNULL AND + "issue_id" ISNULL AND + "state" ISNULL AND + "initiative_id" ISNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" NOTNULL AND + "old_text_value" NOTNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_interest" CHECK ( + "event" != 'interest' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" ISNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" NOTNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_initiator" CHECK ( + "event" != 'initiator' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" NOTNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" NOTNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_support" CHECK ( + "event" != 'support' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" NOTNULL AND + ("draft_id" NOTNULL) = ("boolean_value" = TRUE) AND + "suggestion_id" ISNULL AND + "boolean_value" NOTNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_support_updated" CHECK ( + "event" != 'support_updated' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" NOTNULL AND + "draft_id" NOTNULL AND + "suggestion_id" ISNULL AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_suggestion_rated" CHECK ( + "event" != 'suggestion_rated' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "unit_id" NOTNULL AND + "area_id" NOTNULL AND + "policy_id" NOTNULL AND + "issue_id" NOTNULL AND + "state" NOTNULL AND + "initiative_id" NOTNULL AND + "draft_id" ISNULL AND + "suggestion_id" NOTNULL AND + ("boolean_value" NOTNULL) = ("numeric_value" != 0) AND + "numeric_value" NOTNULL AND + "numeric_value" IN (-2, -1, 0, 1, 2) AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_delegation" CHECK ( + "event" != 'delegation' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + (("other_member_id" ISNULL) OR ("boolean_value" = TRUE)) AND + "scope" NOTNULL AND + "unit_id" NOTNULL AND + ("area_id" NOTNULL) = ("scope" != 'unit'::"delegation_scope") AND + "policy_id" ISNULL AND + ("issue_id" NOTNULL) = ("scope" = 'issue'::"delegation_scope") AND + ("state" NOTNULL) = ("scope" = 'issue'::"delegation_scope") AND + "initiative_id" ISNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" NOTNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_contact" CHECK ( + "event" != 'contact' OR ( + "posting_id" ISNULL AND + "member_id" NOTNULL AND + "other_member_id" NOTNULL AND + "scope" ISNULL AND + "unit_id" ISNULL AND + "area_id" ISNULL AND + "policy_id" ISNULL AND + "issue_id" ISNULL AND + "state" ISNULL AND + "initiative_id" ISNULL AND + "draft_id" ISNULL AND + "suggestion_id" ISNULL AND + "boolean_value" NOTNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); +ALTER TABLE "event" ADD + CONSTRAINT "constr_for_posting_created" CHECK ( + "event" != 'posting_created' OR ( + "posting_id" NOTNULL AND + "member_id" NOTNULL AND + "other_member_id" ISNULL AND + "scope" ISNULL AND + "state" ISNULL AND + ("area_id" ISNULL OR "unit_id" NOTNULL) AND + ("policy_id" NOTNULL) = ("issue_id" NOTNULL) AND + ("issue_id" ISNULL OR "area_id" NOTNULL) AND + ("state" NOTNULL) = ("issue_id" NOTNULL) AND + ("initiative_id" ISNULL OR "issue_id" NOTNULL) AND + "draft_id" ISNULL AND + ("suggestion_id" ISNULL OR "initiative_id" NOTNULL) AND + "boolean_value" ISNULL AND + "numeric_value" ISNULL AND + "text_value" ISNULL AND + "old_text_value" ISNULL )); + +CREATE INDEX "event_tl_global_idx" ON "event" USING gist ((pstamp("member_id", "id"))); +CREATE INDEX "event_tl_unit_idx" ON "event" USING gist ("unit_id", (pstamp("member_id", "id"))) WHERE "unit_id" NOTNULL; +CREATE INDEX "event_tl_area_idx" ON "event" USING gist ("area_id", (pstamp("member_id", "id"))) WHERE "area_id" NOTNULL; +CREATE INDEX "event_tl_policy_idx" ON "event" USING gist ("policy_id", (pstamp("member_id", "id"))) WHERE "policy_id" NOTNULL; +CREATE INDEX "event_tl_issue_idx" ON "event" USING gist ("issue_id", (pstamp("member_id", "id"))) WHERE "issue_id" NOTNULL; +CREATE INDEX "event_tl_initiative_idx" ON "event" USING gist ("initiative_id", (pstamp("member_id", "id"))) WHERE "initiative_id" NOTNULL; +CREATE INDEX "event_tl_suggestion_idx" ON "event" USING gist ("suggestion_id", (pstamp("member_id", "id"))) WHERE "suggestion_id" NOTNULL; + +CREATE OR REPLACE FUNCTION "highlight" + ( "body_p" TEXT, + "query_text_p" TEXT ) + RETURNS TEXT + LANGUAGE 'plpgsql' IMMUTABLE AS $$ + BEGIN + RETURN ts_headline( + replace(replace("body_p", e'\\', e'\\\\'), '*', e'\\*'), + "plainto_tsquery"("query_text_p"), + 'StartSel=* StopSel=* HighlightAll=TRUE' ); + END; + $$; + +CREATE FUNCTION "to_tsvector"("member") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."name", + $1."identification" + )) $$; +CREATE INDEX "member_to_tsvector_idx" ON "member" USING gin + (("to_tsvector"("member".*))); + +CREATE FUNCTION "to_tsvector"("member_profile") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."statement", + $1."profile_text_data" + )) $$; +CREATE INDEX "member_profile_to_tsvector_idx" ON "member_profile" USING gin + (("to_tsvector"("member_profile".*))); + +CREATE FUNCTION "to_tsvector"("unit") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."name", + $1."description" + )) $$; +CREATE INDEX "unit_to_tsvector_idx" ON "unit" USING gin + (("to_tsvector"("unit".*))); + +CREATE FUNCTION "to_tsvector"("area") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."name", + $1."description" + )) $$; +CREATE INDEX "area_to_tsvector_idx" ON "area" USING gin + (("to_tsvector"("area".*))); + +CREATE FUNCTION "to_tsvector"("initiative") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."name", + $1."content" + )) $$; +CREATE INDEX "initiative_to_tsvector_idx" ON "initiative" USING gin + (("to_tsvector"("initiative".*))); + +CREATE FUNCTION "to_tsvector"("draft") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."content" + )) $$; +CREATE INDEX "draft_to_tsvector_idx" ON "draft" USING gin + (("to_tsvector"("draft".*))); + +CREATE FUNCTION "to_tsvector"("suggestion") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."name", + $1."content" + )) $$; +CREATE INDEX "suggestion_to_tsvector_idx" ON "suggestion" USING gin + (("to_tsvector"("suggestion".*))); + +CREATE FUNCTION "to_tsvector"("direct_voter") RETURNS TSVECTOR + LANGUAGE SQL IMMUTABLE AS $$ SELECT to_tsvector(concat_ws(' ', + $1."comment" + )) $$; +CREATE INDEX "direct_voter_to_tsvector_idx" ON "direct_voter" USING gin + (("to_tsvector"("direct_voter".*))); + +CREATE FUNCTION "update_posting_lexeme_trigger"() + RETURNS TRIGGER + LANGUAGE 'plpgsql' VOLATILE AS $$ + DECLARE + "lexeme_v" TEXT; + BEGIN + IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN + DELETE FROM "posting_lexeme" WHERE "posting_id" = OLD."id"; + END IF; + IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN + FOR "lexeme_v" IN + SELECT regexp_matches[1] + FROM regexp_matches(NEW."message", '#[^\s.,;:]+') + LOOP + INSERT INTO "posting_lexeme" ("posting_id", "author_id", "lexeme") + VALUES ( + NEW."id", + NEW."author_id", + "lexeme_v" ) + ON CONFLICT ("posting_id", "lexeme") DO NOTHING; + END LOOP; + END IF; + RETURN NULL; + END; + $$; + +CREATE TRIGGER "update_posting_lexeme" + AFTER INSERT OR UPDATE OR DELETE ON "posting" + FOR EACH ROW EXECUTE PROCEDURE "update_posting_lexeme_trigger"(); + +COMMENT ON FUNCTION "update_posting_lexeme_trigger"() IS 'Implementation of trigger "update_posting_lexeme" on table "posting"'; +COMMENT ON TRIGGER "update_posting_lexeme" ON "posting" IS 'Keeps table "posting_lexeme" up to date'; + +CREATE FUNCTION "write_event_posting_trigger"() + RETURNS TRIGGER + LANGUAGE 'plpgsql' VOLATILE AS $$ + BEGIN + INSERT INTO "event" ( + "event", "posting_id", "member_id", + "unit_id", "area_id", "policy_id", + "issue_id", "initiative_id", "suggestion_id" + ) VALUES ( + 'posting_created', NEW."id", NEW."author_id", + NEW."unit_id", NEW."area_id", NEW."policy_id", + NEW."issue_id", NEW."initiative_id", NEW."suggestion_id" + ); + RETURN NULL; + END; + $$; + +CREATE TRIGGER "write_event_posting" + AFTER INSERT ON "posting" FOR EACH ROW EXECUTE PROCEDURE + "write_event_posting_trigger"(); + +COMMENT ON FUNCTION "write_event_posting_trigger"() IS 'Implementation of trigger "write_event_posting" on table "posting"'; +COMMENT ON TRIGGER "write_event_posting" ON "posting" IS 'Create entry in "event" table when creating a new posting'; + +CREATE FUNCTION "file_requires_reference_trigger"() + RETURNS TRIGGER + LANGUAGE 'plpgsql' VOLATILE AS $$ + BEGIN + IF NOT EXISTS ( + SELECT NULL FROM "draft_attachment" WHERE "file_id" = NEW."id" + ) THEN + RAISE EXCEPTION 'Cannot create an unreferenced file.' USING + ERRCODE = 'integrity_constraint_violation', + HINT = 'Create file and its reference in another table within the same transaction.'; + END IF; + RETURN NULL; + END; + $$; + +CREATE CONSTRAINT TRIGGER "file_requires_reference" + AFTER INSERT OR UPDATE ON "file" DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE + "file_requires_reference_trigger"(); + +COMMENT ON FUNCTION "file_requires_reference_trigger"() IS 'Implementation of trigger "file_requires_reference" on table "file"'; +COMMENT ON TRIGGER "file_requires_reference" ON "file" IS 'Ensure that files are always referenced'; + +CREATE FUNCTION "last_reference_deletes_file_trigger"() + RETURNS TRIGGER + LANGUAGE 'plpgsql' VOLATILE AS $$ + DECLARE + "reference_lost" BOOLEAN; + BEGIN + IF TG_OP = 'DELETE' THEN + "reference_lost" := TRUE; + ELSE + "reference_lost" := NEW."file_id" != OLD."file_id"; + END IF; + IF + "reference_lost" AND NOT EXISTS ( + SELECT NULL FROM "draft_attachment" WHERE "file_id" = OLD."file_id" + ) + THEN + DELETE FROM "file" WHERE "id" = OLD."file_id"; + END IF; + RETURN NULL; + END; + $$; + +CREATE CONSTRAINT TRIGGER "last_reference_deletes_file" + AFTER UPDATE OR DELETE ON "draft_attachment" DEFERRABLE INITIALLY DEFERRED + FOR EACH ROW EXECUTE PROCEDURE + "last_reference_deletes_file_trigger"(); + +COMMENT ON FUNCTION "last_reference_deletes_file_trigger"() IS 'Implementation of trigger "last_reference_deletes_file" on table "draft_attachment"'; +COMMENT ON TRIGGER "last_reference_deletes_file" ON "draft_attachment" IS 'Removing the last reference to a file deletes the file'; + +CREATE OR REPLACE FUNCTION "copy_current_draft_data" + ("initiative_id_p" "initiative"."id"%TYPE ) + RETURNS VOID + LANGUAGE 'plpgsql' VOLATILE AS $$ + BEGIN + PERFORM NULL FROM "initiative" WHERE "id" = "initiative_id_p" + FOR UPDATE; + UPDATE "initiative" SET + "location" = "draft"."location", + "content" = "draft"."content" + FROM "current_draft" AS "draft" + WHERE "initiative"."id" = "initiative_id_p" + AND "draft"."initiative_id" = "initiative_id_p"; + END; + $$; + +CREATE VIEW "follower" AS + SELECT + "id" AS "follower_id", + ( SELECT ARRAY["member"."id"] || array_agg("contact"."other_member_id") + FROM "contact" + WHERE "contact"."member_id" = "member"."id" AND "contact"."following" ) + AS "following_ids" + FROM "member"; + +COMMENT ON VIEW "follower" IS 'Provides the contacts of each member that are being followed (including the member itself) as an array of IDs'; + +CREATE OR REPLACE FUNCTION "check_issue" + ( "issue_id_p" "issue"."id"%TYPE, + "persist" "check_issue_persistence" ) + RETURNS "check_issue_persistence" + LANGUAGE 'plpgsql' VOLATILE AS $$ + DECLARE + "issue_row" "issue"%ROWTYPE; + "last_calculated_v" "snapshot"."calculated"%TYPE; + "policy_row" "policy"%ROWTYPE; + "initiative_row" "initiative"%ROWTYPE; + "state_v" "issue_state"; + BEGIN + PERFORM "require_transaction_isolation"(); + IF "persist" ISNULL THEN + SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p" + FOR UPDATE; + SELECT "calculated" INTO "last_calculated_v" + FROM "snapshot" JOIN "snapshot_issue" + ON "snapshot"."id" = "snapshot_issue"."snapshot_id" + WHERE "snapshot_issue"."issue_id" = "issue_id_p" + ORDER BY "snapshot"."id" DESC; + IF "issue_row"."closed" NOTNULL THEN + RETURN NULL; + END IF; + "persist"."state" := "issue_row"."state"; + IF + ( "issue_row"."state" = 'admission' AND "last_calculated_v" >= + "issue_row"."created" + "issue_row"."max_admission_time" ) OR + ( "issue_row"."state" = 'discussion' AND now() >= + "issue_row"."accepted" + "issue_row"."discussion_time" ) OR + ( "issue_row"."state" = 'verification' AND now() >= + "issue_row"."half_frozen" + "issue_row"."verification_time" ) OR + ( "issue_row"."state" = 'voting' AND now() >= + "issue_row"."fully_frozen" + "issue_row"."voting_time" ) + THEN + "persist"."phase_finished" := TRUE; + ELSE + "persist"."phase_finished" := FALSE; + END IF; + IF + NOT EXISTS ( + -- all initiatives are revoked + SELECT NULL FROM "initiative" + WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL + ) AND ( + -- and issue has not been accepted yet + "persist"."state" = 'admission' OR + -- or verification time has elapsed + ( "persist"."state" = 'verification' AND + "persist"."phase_finished" ) OR + -- or no initiatives have been revoked lately + NOT EXISTS ( + SELECT NULL FROM "initiative" + WHERE "issue_id" = "issue_id_p" + AND now() < "revoked" + "issue_row"."verification_time" + ) + ) + THEN + "persist"."issue_revoked" := TRUE; + ELSE + "persist"."issue_revoked" := FALSE; + END IF; + IF "persist"."phase_finished" OR "persist"."issue_revoked" THEN + UPDATE "issue" SET "phase_finished" = now() + WHERE "id" = "issue_row"."id"; + RETURN "persist"; + ELSIF + "persist"."state" IN ('admission', 'discussion', 'verification') + THEN + RETURN "persist"; + ELSE + RETURN NULL; + END IF; + END IF; + IF + "persist"."state" IN ('admission', 'discussion', 'verification') AND + coalesce("persist"."snapshot_created", FALSE) = FALSE + THEN + IF "persist"."state" != 'admission' THEN + PERFORM "take_snapshot"("issue_id_p"); + PERFORM "finish_snapshot"("issue_id_p"); + ELSE + UPDATE "issue" SET "issue_quorum" = "issue_quorum"."issue_quorum" + FROM "issue_quorum" + WHERE "id" = "issue_id_p" + AND "issue_quorum"."issue_id" = "issue_id_p"; + END IF; + "persist"."snapshot_created" = TRUE; + IF "persist"."phase_finished" THEN + IF "persist"."state" = 'admission' THEN + UPDATE "issue" SET "admission_snapshot_id" = "latest_snapshot_id" + WHERE "id" = "issue_id_p"; + ELSIF "persist"."state" = 'discussion' THEN + UPDATE "issue" SET "half_freeze_snapshot_id" = "latest_snapshot_id" + WHERE "id" = "issue_id_p"; + ELSIF "persist"."state" = 'verification' THEN + UPDATE "issue" SET "full_freeze_snapshot_id" = "latest_snapshot_id" + WHERE "id" = "issue_id_p"; + SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p"; + FOR "initiative_row" IN + SELECT * FROM "initiative" + WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL + FOR UPDATE + LOOP + IF + "initiative_row"."polling" OR + "initiative_row"."satisfied_supporter_count" >= + "issue_row"."initiative_quorum" + THEN + UPDATE "initiative" SET "admitted" = TRUE + WHERE "id" = "initiative_row"."id"; + ELSE + UPDATE "initiative" SET "admitted" = FALSE + WHERE "id" = "initiative_row"."id"; + END IF; + END LOOP; + END IF; + END IF; + RETURN "persist"; + END IF; + IF + "persist"."state" IN ('admission', 'discussion', 'verification') AND + coalesce("persist"."harmonic_weights_set", FALSE) = FALSE + THEN + PERFORM "set_harmonic_initiative_weights"("issue_id_p"); + "persist"."harmonic_weights_set" = TRUE; + IF + "persist"."phase_finished" OR + "persist"."issue_revoked" OR + "persist"."state" = 'admission' + THEN + RETURN "persist"; + ELSE + RETURN NULL; + END IF; + END IF; + IF "persist"."issue_revoked" THEN + IF "persist"."state" = 'admission' THEN + "state_v" := 'canceled_revoked_before_accepted'; + ELSIF "persist"."state" = 'discussion' THEN + "state_v" := 'canceled_after_revocation_during_discussion'; + ELSIF "persist"."state" = 'verification' THEN + "state_v" := 'canceled_after_revocation_during_verification'; + END IF; + UPDATE "issue" SET + "state" = "state_v", + "closed" = "phase_finished", + "phase_finished" = NULL + WHERE "id" = "issue_id_p"; + RETURN NULL; + END IF; + IF "persist"."state" = 'admission' THEN + SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p" + FOR UPDATE; + IF "issue_row"."phase_finished" NOTNULL THEN + UPDATE "issue" SET + "state" = 'canceled_issue_not_accepted', + "closed" = "phase_finished", + "phase_finished" = NULL + WHERE "id" = "issue_id_p"; + END IF; + RETURN NULL; + END IF; + IF "persist"."phase_finished" THEN + IF "persist"."state" = 'discussion' THEN + UPDATE "issue" SET + "state" = 'verification', + "half_frozen" = "phase_finished", + "phase_finished" = NULL + WHERE "id" = "issue_id_p"; + RETURN NULL; + END IF; + IF "persist"."state" = 'verification' THEN + SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p" + FOR UPDATE; + SELECT * INTO "policy_row" FROM "policy" + WHERE "id" = "issue_row"."policy_id"; + IF EXISTS ( + SELECT NULL FROM "initiative" + WHERE "issue_id" = "issue_id_p" AND "admitted" = TRUE + ) THEN + UPDATE "issue" SET + "state" = 'voting', + "fully_frozen" = "phase_finished", + "phase_finished" = NULL + WHERE "id" = "issue_id_p"; + ELSE + UPDATE "issue" SET + "state" = 'canceled_no_initiative_admitted', + "fully_frozen" = "phase_finished", + "closed" = "phase_finished", + "phase_finished" = NULL + WHERE "id" = "issue_id_p"; + -- NOTE: The following DELETE statements have effect only when + -- issue state has been manipulated + DELETE FROM "direct_voter" WHERE "issue_id" = "issue_id_p"; + DELETE FROM "delegating_voter" WHERE "issue_id" = "issue_id_p"; + DELETE FROM "battle" WHERE "issue_id" = "issue_id_p"; + END IF; + RETURN NULL; + END IF; + IF "persist"."state" = 'voting' THEN + IF coalesce("persist"."closed_voting", FALSE) = FALSE THEN + PERFORM "close_voting"("issue_id_p"); + "persist"."closed_voting" = TRUE; + RETURN "persist"; + END IF; + PERFORM "calculate_ranks"("issue_id_p"); + RETURN NULL; + END IF; + END IF; + RAISE WARNING 'should not happen'; + RETURN NULL; + END; + $$; + +CREATE OR REPLACE FUNCTION "check_everything"() + RETURNS VOID + LANGUAGE 'plpgsql' VOLATILE AS $$ + DECLARE + "area_id_v" "area"."id"%TYPE; + "snapshot_id_v" "snapshot"."id"%TYPE; + "issue_id_v" "issue"."id"%TYPE; + "persist_v" "check_issue_persistence"; + BEGIN + RAISE WARNING 'Function "check_everything" should only be used for development and debugging purposes'; + DELETE FROM "expired_session"; + DELETE FROM "expired_token"; + DELETE FROM "unused_snapshot"; + PERFORM "check_activity"(); + PERFORM "calculate_member_counts"(); + FOR "area_id_v" IN SELECT "id" FROM "area_with_unaccepted_issues" LOOP + SELECT "take_snapshot"(NULL, "area_id_v") INTO "snapshot_id_v"; + PERFORM "finish_snapshot"("issue_id") FROM "snapshot_issue" + WHERE "snapshot_id" = "snapshot_id_v"; + LOOP + EXIT WHEN "issue_admission"("area_id_v") = FALSE; + END LOOP; + END LOOP; + FOR "issue_id_v" IN SELECT "id" FROM "open_issue" LOOP + "persist_v" := NULL; + LOOP + "persist_v" := "check_issue"("issue_id_v", "persist_v"); + EXIT WHEN "persist_v" ISNULL; + END LOOP; + END LOOP; + DELETE FROM "unused_snapshot"; + RETURN; + END; + $$; + +CREATE OR REPLACE FUNCTION "delete_member"("member_id_p" "member"."id"%TYPE) + RETURNS VOID + LANGUAGE 'plpgsql' VOLATILE AS $$ + BEGIN + UPDATE "member" SET + "last_login" = NULL, + "last_delegation_check" = NULL, + "login" = NULL, + "password" = NULL, + "authority" = NULL, + "authority_uid" = NULL, + "authority_login" = NULL, + "deleted" = coalesce("deleted", now()), + "locked" = TRUE, + "active" = FALSE, + "notify_email" = NULL, + "notify_email_unconfirmed" = NULL, + "notify_email_secret" = NULL, + "notify_email_secret_expiry" = NULL, + "notify_email_lock_expiry" = NULL, + "disable_notifications" = TRUE, + "notification_counter" = DEFAULT, + "notification_sample_size" = 0, + "notification_dow" = NULL, + "notification_hour" = NULL, + "notification_sent" = NULL, + "login_recovery_expiry" = NULL, + "password_reset_secret" = NULL, + "password_reset_secret_expiry" = NULL, + "location" = NULL + WHERE "id" = "member_id_p"; + DELETE FROM "member_settings" WHERE "member_id" = "member_id_p"; + DELETE FROM "member_profile" WHERE "member_id" = "member_id_p"; + DELETE FROM "rendered_member_statement" WHERE "member_id" = "member_id_p"; + DELETE FROM "member_image" WHERE "member_id" = "member_id_p"; + DELETE FROM "contact" WHERE "member_id" = "member_id_p"; + DELETE FROM "ignored_member" WHERE "member_id" = "member_id_p"; + DELETE FROM "session" WHERE "member_id" = "member_id_p"; + DELETE FROM "member_application" WHERE "member_id" = "member_id_p"; + DELETE FROM "token" WHERE "member_id" = "member_id_p"; + DELETE FROM "subscription" WHERE "member_id" = "member_id_p"; + DELETE FROM "ignored_area" WHERE "member_id" = "member_id_p"; + DELETE FROM "ignored_initiative" WHERE "member_id" = "member_id_p"; + DELETE FROM "delegation" WHERE "truster_id" = "member_id_p"; + DELETE FROM "non_voter" WHERE "member_id" = "member_id_p"; + DELETE FROM "direct_voter" USING "issue" + WHERE "direct_voter"."issue_id" = "issue"."id" + AND "issue"."closed" ISNULL + AND "member_id" = "member_id_p"; + DELETE FROM "notification_initiative_sent" WHERE "member_id" = "member_id_p"; + RETURN; + END; + $$; + +CREATE OR REPLACE FUNCTION "delete_private_data"() + RETURNS VOID + LANGUAGE 'plpgsql' VOLATILE AS $$ + BEGIN + DELETE FROM "temporary_transaction_data"; + DELETE FROM "temporary_suggestion_counts"; + DELETE FROM "member" WHERE "activated" ISNULL; + UPDATE "member" SET + "invite_code" = NULL, + "invite_code_expiry" = NULL, + "admin_comment" = NULL, + "last_login" = NULL, + "last_delegation_check" = NULL, + "login" = NULL, + "password" = NULL, + "authority" = NULL, + "authority_uid" = NULL, + "authority_login" = NULL, + "lang" = NULL, + "notify_email" = NULL, + "notify_email_unconfirmed" = NULL, + "notify_email_secret" = NULL, + "notify_email_secret_expiry" = NULL, + "notify_email_lock_expiry" = NULL, + "disable_notifications" = TRUE, + "notification_counter" = DEFAULT, + "notification_sample_size" = 0, + "notification_dow" = NULL, + "notification_hour" = NULL, + "notification_sent" = NULL, + "login_recovery_expiry" = NULL, + "password_reset_secret" = NULL, + "password_reset_secret_expiry" = NULL, + "location" = NULL; + DELETE FROM "verification"; + DELETE FROM "member_settings"; + DELETE FROM "member_useterms"; + DELETE FROM "member_profile"; + DELETE FROM "rendered_member_statement"; + DELETE FROM "member_image"; + DELETE FROM "contact"; + DELETE FROM "ignored_member"; + DELETE FROM "session"; + DELETE FROM "system_application"; + DELETE FROM "system_application_redirect_uri"; + DELETE FROM "dynamic_application_scope"; + DELETE FROM "member_application"; + DELETE FROM "token"; + DELETE FROM "subscription"; + DELETE FROM "ignored_area"; + DELETE FROM "ignored_initiative"; + DELETE FROM "non_voter"; + DELETE FROM "direct_voter" USING "issue" + WHERE "direct_voter"."issue_id" = "issue"."id" + AND "issue"."closed" ISNULL; + DELETE FROM "event_processed"; + DELETE FROM "notification_initiative_sent"; + DELETE FROM "newsletter"; + RETURN; + END; + $$; + +CREATE VIEW "member_eligible_to_be_notified" AS + SELECT * FROM "member" + WHERE "activated" NOTNULL AND "locked" = FALSE; + +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")'; + +CREATE VIEW "member_to_notify" AS + SELECT * FROM "member_eligible_to_be_notified" + WHERE "disable_notifications" = FALSE; + +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)'; + +CREATE VIEW "area_with_unaccepted_issues" AS + SELECT DISTINCT ON ("area"."id") "area".* + FROM "area" JOIN "issue" ON "area"."id" = "issue"."area_id" + WHERE "issue"."state" = 'admission'; + +COMMENT ON VIEW "area_with_unaccepted_issues" IS 'All areas with unaccepted open issues (needed for issue admission system)'; + +CREATE VIEW "opening_draft" AS + SELECT DISTINCT ON ("initiative_id") * FROM "draft" + ORDER BY "initiative_id", "id"; + +COMMENT ON VIEW "opening_draft" IS 'First drafts of all initiatives'; + +CREATE VIEW "current_draft" AS + SELECT DISTINCT ON ("initiative_id") * FROM "draft" + ORDER BY "initiative_id", "id" DESC; + +COMMENT ON VIEW "current_draft" IS 'All latest drafts for each initiative'; + +CREATE VIEW "member_contingent" AS + SELECT + "member"."id" AS "member_id", + "contingent"."polling", + "contingent"."time_frame", + CASE WHEN "contingent"."text_entry_limit" NOTNULL THEN + ( + SELECT count(1) FROM "draft" + JOIN "initiative" ON "initiative"."id" = "draft"."initiative_id" + WHERE "draft"."author_id" = "member"."id" + AND "initiative"."polling" = "contingent"."polling" + AND "draft"."created" > now() - "contingent"."time_frame" + ) + ( + SELECT count(1) FROM "suggestion" + JOIN "initiative" ON "initiative"."id" = "suggestion"."initiative_id" + WHERE "suggestion"."author_id" = "member"."id" + AND "contingent"."polling" = FALSE + AND "suggestion"."created" > now() - "contingent"."time_frame" + ) + ELSE NULL END AS "text_entry_count", + "contingent"."text_entry_limit", + CASE WHEN "contingent"."initiative_limit" NOTNULL THEN ( + SELECT count(1) FROM "opening_draft" AS "draft" + JOIN "initiative" ON "initiative"."id" = "draft"."initiative_id" + WHERE "draft"."author_id" = "member"."id" + AND "initiative"."polling" = "contingent"."polling" + AND "draft"."created" > now() - "contingent"."time_frame" + ) ELSE NULL END AS "initiative_count", + "contingent"."initiative_limit" + FROM "member" CROSS JOIN "contingent"; + +COMMENT ON VIEW "member_contingent" IS 'Actual counts of text entries and initiatives are calculated per member for each limit in the "contingent" table.'; + +COMMENT ON COLUMN "member_contingent"."text_entry_count" IS 'Only calculated when "text_entry_limit" is not null in the same row'; +COMMENT ON COLUMN "member_contingent"."initiative_count" IS 'Only calculated when "initiative_limit" is not null in the same row'; + +CREATE VIEW "member_contingent_left" AS + SELECT + "member_id", + "polling", + max("text_entry_limit" - "text_entry_count") AS "text_entries_left", + max("initiative_limit" - "initiative_count") AS "initiatives_left" + FROM "member_contingent" GROUP BY "member_id", "polling"; + +COMMENT ON VIEW "member_contingent_left" IS 'Amount of text entries or initiatives which can be posted now instantly by a member. This view should be used by a frontend to determine, if the contingent for posting is exhausted.'; + +CREATE VIEW "scheduled_notification_to_send" AS + SELECT * FROM ( + SELECT + "id" AS "recipient_id", + now() - CASE WHEN "notification_dow" ISNULL THEN + ( "notification_sent"::DATE + CASE + WHEN EXTRACT(HOUR FROM "notification_sent") < "notification_hour" + THEN 0 ELSE 1 END + )::TIMESTAMP + '1 hour'::INTERVAL * "notification_hour" + ELSE + ( "notification_sent"::DATE + + ( 7 + "notification_dow" - + EXTRACT(DOW FROM + ( "notification_sent"::DATE + CASE + WHEN EXTRACT(HOUR FROM "notification_sent") < "notification_hour" + THEN 0 ELSE 1 END + )::TIMESTAMP + '1 hour'::INTERVAL * "notification_hour" + )::INTEGER + ) % 7 + + CASE + WHEN EXTRACT(HOUR FROM "notification_sent") < "notification_hour" + THEN 0 ELSE 1 + END + )::TIMESTAMP + '1 hour'::INTERVAL * "notification_hour" + END AS "pending" + FROM ( + SELECT + "id", + COALESCE("notification_sent", "activated") AS "notification_sent", + "notification_dow", + "notification_hour" + FROM "member_to_notify" + WHERE "notification_hour" NOTNULL + ) AS "subquery1" + ) AS "subquery2" + WHERE "pending" > '0'::INTERVAL; + +COMMENT ON VIEW "scheduled_notification_to_send" IS 'Set of members where a scheduled notification mail is pending'; + +COMMENT ON COLUMN "scheduled_notification_to_send"."recipient_id" IS '"id" of the member who needs to receive a notification mail'; +COMMENT ON COLUMN "scheduled_notification_to_send"."pending" IS 'Duration for which the notification mail has already been pending'; + +CREATE VIEW "newsletter_to_send" AS + SELECT + "member"."id" AS "recipient_id", + "newsletter"."id" AS "newsletter_id", + "newsletter"."published" + FROM "newsletter" CROSS JOIN "member_eligible_to_be_notified" AS "member" + LEFT JOIN "privilege" ON + "privilege"."member_id" = "member"."id" AND + "privilege"."unit_id" = "newsletter"."unit_id" AND + "privilege"."voting_right" = TRUE + LEFT JOIN "subscription" ON + "subscription"."member_id" = "member"."id" AND + "subscription"."unit_id" = "newsletter"."unit_id" + WHERE "newsletter"."published" <= now() + AND "newsletter"."sent" ISNULL + AND ( + "member"."disable_notifications" = FALSE OR + "newsletter"."include_all_members" = TRUE ) + AND ( + "newsletter"."unit_id" ISNULL OR + "privilege"."member_id" NOTNULL OR + "subscription"."member_id" NOTNULL ); + +COMMENT ON VIEW "newsletter_to_send" IS 'List of "newsletter_id"s for each member that are due to be sent out'; + +COMMENT ON COLUMN "newsletter"."published" IS 'Timestamp when the newsletter was supposed to be sent out (can be used for ordering)'; + +SELECT "copy_current_draft_data" ("id") FROM "initiative"; + +END;