-- ===================================================================== -- WebGIS Citizen Participation Portal — Initial Schema -- Migration: 001_initial_schema.sql -- Description: Creates Core Tables for a multi-tenant Citizen -- Participation Platform with Point/Line/Polygon -- Contributions, Voting, and Moderation Workflow. -- ===================================================================== -- --------------------------------------------------------------------- -- Block 1: Checks PostGIS Extension -- --------------------------------------------------------------------- CREATE EXTENSION IF NOT EXISTS postgis; -- --------------------------------------------------------------------- -- Block 2: Creates Table "municipalities" -- One Row per Municipalitiy using the Portal (multi-tenant setup). -- --------------------------------------------------------------------- CREATE TABLE municipalities ( municipality_id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, -- Municipalitiy Name slug VARCHAR(50) NOT NULL UNIQUE, -- URL-safe Identifier, e.g. "lohne" center_lat DOUBLE PRECISION NOT NULL, -- Map Center Latitude center_lng DOUBLE PRECISION NOT NULL, -- Map Center Longitude default_zoom SMALLINT NOT NULL DEFAULT 13, -- Map Default Zoom Level logo_path VARCHAR(255), -- Relative Path to Municipality Logo primary_color VARCHAR(7) DEFAULT '#6a6a6a', -- HexColor for UI Theme created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE municipalities IS 'Configuration Per Municipality (Tenant) using the Citizen Participation Portal.'; -- --------------------------------------------------------------------- -- Block 3: Table "contributions" -- Aitizen and Administration Contributions as Points, Lines, and -- Polygons stored together in one mixed-geometry Column. -- --------------------------------------------------------------------- CREATE TABLE contributions ( contribution_id SERIAL PRIMARY KEY, municipality_id INTEGER NOT NULL REFERENCES municipalities(municipality_id) ON DELETE CASCADE, geom GEOMETRY(Geometry, 4326) NOT NULL, -- Mixed Geometry: Point, Line, Polygon, ... (WGS84) geom_type VARCHAR(20) NOT NULL, -- 'point' | 'line' | 'polygon' category VARCHAR(50) NOT NULL, -- Contribution Category title VARCHAR(200) NOT NULL, description TEXT, author_name VARCHAR(100) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', likes_count INTEGER NOT NULL DEFAULT 0, dislikes_count INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT contributions_geom_type_check CHECK (geom_type IN ('point', 'line', 'polygon')), CONSTRAINT contributions_status_check CHECK (status IN ('pending', 'approved', 'rejected', 'in_progress', 'done')) ); COMMENT ON TABLE contributions IS 'Citizen and Administration Contributions with mixed Geometry Types.'; -- --------------------------------------------------------------------- -- Block 4: Indexes for fast Queries -- --------------------------------------------------------------------- CREATE INDEX contributions_geom_idx ON contributions USING GIST (geom); CREATE INDEX contributions_municipality_idx ON contributions (municipality_id); CREATE INDEX contributions_status_idx ON contributions (status); CREATE INDEX contributions_category_idx ON contributions (category); -- --------------------------------------------------------------------- -- Block 5: Table "votes" -- Individual like and dislike Records. UNIQUE Constraint prevents the -- same voter from liking or disliking the same contribution multiple times. -- --------------------------------------------------------------------- CREATE TABLE votes ( vote_id SERIAL PRIMARY KEY, contribution_id INTEGER NOT NULL REFERENCES contributions(contribution_id) ON DELETE CASCADE, voter_name VARCHAR(100) NOT NULL, -- ToDo: Replace with user_id once Authentification exists vote_type VARCHAR(10) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT votes_unique_per_voter UNIQUE (contribution_id, voter_name), CONSTRAINT votes_vote_type_check CHECK (vote_type IN ('like', 'dislike')) ); COMMENT ON TABLE votes IS 'Individual Votes to prevent duplicate Likes and Dislikes.'; -- --------------------------------------------------------------------- -- Block 6: Trigger Functions -- --------------------------------------------------------------------- -- Automatically Refresh updated_at on every UPDATE. CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER contributions_updated_at BEFORE UPDATE ON contributions FOR EACH ROW EXECUTE FUNCTION set_updated_at(); CREATE TRIGGER municipalities_updated_at BEFORE UPDATE ON municipalities FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- Keeps likes_count / dislikes_count synchronized with the votes Table. CREATE OR REPLACE FUNCTION update_vote_counts() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN IF NEW.vote_type = 'like' THEN UPDATE contributions SET likes_count = likes_count + 1 WHERE contribution_id = NEW.contribution_id; ELSE UPDATE contributions SET dislikes_count = dislikes_count + 1 WHERE contribution_id = NEW.contribution_id; END IF; ELSIF TG_OP = 'DELETE' THEN IF OLD.vote_type = 'like' THEN UPDATE contributions SET likes_count = GREATEST(likes_count - 1, 0) WHERE contribution_id = OLD.contribution_id; ELSE UPDATE contributions SET dislikes_count = GREATEST(dislikes_count - 1, 0) WHERE contribution_id = OLD.contribution_id; END IF; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; CREATE TRIGGER votes_count_sync AFTER INSERT OR DELETE ON votes FOR EACH ROW EXECUTE FUNCTION update_vote_counts(); -- --------------------------------------------------------------------- -- Block 7: Typed Geometry Views for QGIS -- QGIS handles mixed-geometry Tables awkwardly, so one View per -- Geometry Type is created. Reflects live Data from the Contributions Table. -- --------------------------------------------------------------------- CREATE VIEW contributions_points AS SELECT * FROM contributions WHERE geom_type = 'point'; CREATE VIEW contributions_lines AS SELECT * FROM contributions WHERE geom_type = 'line'; CREATE VIEW contributions_polygons AS SELECT * FROM contributions WHERE geom_type = 'polygon'; -- --------------------------------------------------------------------- -- Block 8: Seed Data — Initial Municipality -- --------------------------------------------------------------------- INSERT INTO municipalities (name, slug, center_lat, center_lng, default_zoom, primary_color) VALUES ('Lohne (Oldenburg)', 'lohne', 52.66639, 8.23306, 14, '#00376D'); -- ===================================================================== -- End of migration 001_initial_schema.sql -- =====================================================================