diff --git a/Gemfile b/Gemfile index 373ad0c3..8eaeaa2b 100644 --- a/Gemfile +++ b/Gemfile @@ -57,6 +57,7 @@ gem 'aws-sdk' gem 'garb' gem 'oauth' gem 'rails_config' +gem 'activerecord-postgres-hstore' # To use ActiveModel has_secure_password gem 'bcrypt-ruby', '~> 3.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 23802925..09a1927b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,6 +56,10 @@ GEM activesupport (= 3.2.12) arel (~> 3.0.2) tzinfo (~> 0.3.29) + activerecord-postgres-hstore (0.7.6) + activerecord (>= 3.1) + pg-hstore (>= 1.1.5) + rake activeresource (3.2.12) activemodel (= 3.2.12) activesupport (= 3.2.12) @@ -197,6 +201,7 @@ GEM nokogiri (1.5.6) oauth (0.4.7) pg (0.14.1) + pg-hstore (1.2.0) polyglot (0.3.3) premailer (1.7.3) css_parser (>= 1.1.9) @@ -356,6 +361,7 @@ PLATFORMS DEPENDENCIES active_attr + activerecord-postgres-hstore airbrake anjlab-bootstrap-rails (>= 2.2) aws-sdk diff --git a/app/models/app_settings.rb b/app/models/app_settings.rb new file mode 100644 index 00000000..b8bfb047 --- /dev/null +++ b/app/models/app_settings.rb @@ -0,0 +1,32 @@ +class AppSettings < ActiveRecord::Base + serialize :data, ActiveRecord::Coders::Hstore + self.table_name = "settings" + + class << self + + def instance + @instance ||= ( self.first || self.create ) + end + + def create_with_instance_checking(*args) + if self.first.nil? + create_without_instance_checking(*args) + else + raise "Can't create a new instance." + end + end + + alias_method_chain :create, :instance_checking + + def [](key) + self.instance.data[key] + end + + def []=(key, value) + self.instance.data[key] = value + self.instance.save + value + end + + end +end diff --git a/db/migrate/20130807232909_add_hstore_settings_to_app.rb b/db/migrate/20130807232909_add_hstore_settings_to_app.rb new file mode 100644 index 00000000..3537c6dd --- /dev/null +++ b/db/migrate/20130807232909_add_hstore_settings_to_app.rb @@ -0,0 +1,8 @@ +class AddHstoreSettingsToApp < ActiveRecord::Migration + def change + create_table :settings do |t| + t.hstore :data + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index a85ce102..dd88fd83 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -22,29 +22,21 @@ CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; -SET search_path = public, pg_catalog; - -- --- Name: bigint_to_inet(bigint); Type: FUNCTION; Schema: public; Owner: - +-- Name: hstore; Type: EXTENSION; Schema: -; Owner: - -- -CREATE FUNCTION bigint_to_inet(bigint) RETURNS inet - LANGUAGE sql IMMUTABLE STRICT - AS $_$ -SELECT (($1>>24&255)||'.'||($1>>16&255)||'.'||($1>>8&255)||'.'||($1>>0&255))::inet -$_$; +CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public; -- --- Name: inet_to_bigint(inet); Type: FUNCTION; Schema: public; Owner: - +-- Name: EXTENSION hstore; Type: COMMENT; Schema: -; Owner: - -- -CREATE FUNCTION inet_to_bigint(inet) RETURNS bigint - LANGUAGE sql IMMUTABLE STRICT - AS $_$ -SELECT $1 - inet '0.0.0.0' -$_$; +COMMENT ON EXTENSION hstore IS 'data type for storing sets of (key, value) pairs'; + +SET search_path = public, pg_catalog; SET default_tablespace = ''; @@ -98,10 +90,10 @@ CREATE TABLE donations ( -- --- Name: donation_clicks_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- Name: donations_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -CREATE SEQUENCE donation_clicks_id_seq +CREATE SEQUENCE donations_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -110,10 +102,10 @@ CREATE SEQUENCE donation_clicks_id_seq -- --- Name: donation_clicks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: donations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE donation_clicks_id_seq OWNED BY donations.id; +ALTER SEQUENCE donations_id_seq OWNED BY donations.id; -- @@ -198,6 +190,25 @@ CREATE TABLE facebook_actions ( ); +-- +-- Name: facebook_actions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE facebook_actions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: facebook_actions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE facebook_actions_id_seq OWNED BY facebook_actions.id; + + -- -- Name: facebook_friends; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -271,11 +282,11 @@ ALTER SEQUENCE facebook_share_widget_shares_id_seq OWNED BY facebook_share_widge CREATE TABLE ip_locations ( ip_from bigint, ip_to bigint, - country_code character(2), + country_code character varying(2), country_name text, region text, city text, - state_code character(2) + state_code character varying(2) ); @@ -310,25 +321,6 @@ CREATE SEQUENCE last_updated_unsubscribes_id_seq ALTER SEQUENCE last_updated_unsubscribes_id_seq OWNED BY last_updated_unsubscribes.id; --- --- Name: likes_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE likes_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: likes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE likes_id_seq OWNED BY facebook_actions.id; - - -- -- Name: mailer_process_trackers; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -712,10 +704,10 @@ CREATE TABLE referrals ( -- --- Name: referral_codes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- Name: referrals_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -CREATE SEQUENCE referral_codes_id_seq +CREATE SEQUENCE referrals_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -724,10 +716,10 @@ CREATE SEQUENCE referral_codes_id_seq -- --- Name: referral_codes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: referrals_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE referral_codes_id_seq OWNED BY referrals.id; +ALTER SEQUENCE referrals_id_seq OWNED BY referrals.id; -- @@ -776,6 +768,37 @@ CREATE SEQUENCE sent_emails_id_seq ALTER SEQUENCE sent_emails_id_seq OWNED BY sent_emails.id; +-- +-- Name: settings; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE settings ( + id integer NOT NULL, + data hstore, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: settings_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE settings_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: settings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE settings_id_seq OWNED BY settings.id; + + -- -- Name: signatures; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -843,10 +866,10 @@ CREATE TABLE social_media_trials ( -- --- Name: social_media_experiments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- Name: social_media_trials_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- -CREATE SEQUENCE social_media_experiments_id_seq +CREATE SEQUENCE social_media_trials_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -855,10 +878,10 @@ CREATE SEQUENCE social_media_experiments_id_seq -- --- Name: social_media_experiments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: social_media_trials_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- -ALTER SEQUENCE social_media_experiments_id_seq OWNED BY social_media_trials.id; +ALTER SEQUENCE social_media_trials_id_seq OWNED BY social_media_trials.id; -- @@ -1006,7 +1029,7 @@ ALTER TABLE ONLY bounced_emails ALTER COLUMN id SET DEFAULT nextval('bounced_ema -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY donations ALTER COLUMN id SET DEFAULT nextval('donation_clicks_id_seq'::regclass); +ALTER TABLE ONLY donations ALTER COLUMN id SET DEFAULT nextval('donations_id_seq'::regclass); -- @@ -1027,7 +1050,7 @@ ALTER TABLE ONLY email_experiments ALTER COLUMN id SET DEFAULT nextval('email_ex -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY facebook_actions ALTER COLUMN id SET DEFAULT nextval('likes_id_seq'::regclass); +ALTER TABLE ONLY facebook_actions ALTER COLUMN id SET DEFAULT nextval('facebook_actions_id_seq'::regclass); -- @@ -1118,7 +1141,7 @@ ALTER TABLE ONLY petitions ALTER COLUMN id SET DEFAULT nextval('petitions_id_seq -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY referrals ALTER COLUMN id SET DEFAULT nextval('referral_codes_id_seq'::regclass); +ALTER TABLE ONLY referrals ALTER COLUMN id SET DEFAULT nextval('referrals_id_seq'::regclass); -- @@ -1128,6 +1151,13 @@ ALTER TABLE ONLY referrals ALTER COLUMN id SET DEFAULT nextval('referral_codes_i ALTER TABLE ONLY sent_emails ALTER COLUMN id SET DEFAULT nextval('sent_emails_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY settings ALTER COLUMN id SET DEFAULT nextval('settings_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -1139,7 +1169,7 @@ ALTER TABLE ONLY signatures ALTER COLUMN id SET DEFAULT nextval('signatures_id_s -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- -ALTER TABLE ONLY social_media_trials ALTER COLUMN id SET DEFAULT nextval('social_media_experiments_id_seq'::regclass); +ALTER TABLE ONLY social_media_trials ALTER COLUMN id SET DEFAULT nextval('social_media_trials_id_seq'::regclass); -- @@ -1179,11 +1209,11 @@ ALTER TABLE ONLY bounced_emails -- --- Name: donation_clicks_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- Name: donations_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- ALTER TABLE ONLY donations - ADD CONSTRAINT donation_clicks_pkey PRIMARY KEY (id); + ADD CONSTRAINT donations_pkey PRIMARY KEY (id); -- @@ -1202,6 +1232,14 @@ ALTER TABLE ONLY email_experiments ADD CONSTRAINT email_experiments_pkey PRIMARY KEY (id); +-- +-- Name: facebook_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY facebook_actions + ADD CONSTRAINT facebook_actions_pkey PRIMARY KEY (id); + + -- -- Name: facebook_friends_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1226,14 +1264,6 @@ ALTER TABLE ONLY last_updated_unsubscribes ADD CONSTRAINT last_updated_unsubscribes_pkey PRIMARY KEY (id); --- --- Name: likes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: --- - -ALTER TABLE ONLY facebook_actions - ADD CONSTRAINT likes_pkey PRIMARY KEY (id); - - -- -- Name: mailer_process_trackers_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1307,11 +1337,11 @@ ALTER TABLE ONLY petitions -- --- Name: referral_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- Name: referrals_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- ALTER TABLE ONLY referrals - ADD CONSTRAINT referral_codes_pkey PRIMARY KEY (id); + ADD CONSTRAINT referrals_pkey PRIMARY KEY (id); -- @@ -1322,6 +1352,14 @@ ALTER TABLE ONLY sent_emails ADD CONSTRAINT sent_emails_pkey PRIMARY KEY (id); +-- +-- Name: settings_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY settings + ADD CONSTRAINT settings_pkey PRIMARY KEY (id); + + -- -- Name: signatures_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -1331,11 +1369,11 @@ ALTER TABLE ONLY signatures -- --- Name: social_media_experiments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- Name: social_media_trials_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- ALTER TABLE ONLY social_media_trials - ADD CONSTRAINT social_media_experiments_pkey PRIMARY KEY (id); + ADD CONSTRAINT social_media_trials_pkey PRIMARY KEY (id); -- @@ -1398,13 +1436,6 @@ CREATE INDEX index_likes_on_member_id ON facebook_actions USING btree (member_id CREATE INDEX index_likes_on_petition_id ON facebook_actions USING btree (petition_id); --- --- Name: index_lower_members_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace: --- - -CREATE INDEX index_lower_members_on_email ON members USING btree (lower((email)::text)); - - -- -- Name: index_members_on_email; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -1783,13 +1814,6 @@ CREATE INDEX index_unsubscribes_on_member_id_and_created_at ON unsubscribes USIN CREATE INDEX index_unsubscribes_on_sent_email_id ON unsubscribes USING btree (sent_email_id); --- --- Name: ip2location_range_gist; Type: INDEX; Schema: public; Owner: -; Tablespace: --- - -CREATE INDEX ip2location_range_gist ON ip_locations USING gist (box(point((ip_from)::double precision, (ip_from)::double precision), point((ip_to)::double precision, (ip_to)::double precision))); - - -- -- Name: sent_emails_created_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: -- @@ -2085,8 +2109,6 @@ INSERT INTO schema_migrations (version) VALUES ('20120807021552'); INSERT INTO schema_migrations (version) VALUES ('20120813135808'); -INSERT INTO schema_migrations (version) VALUES ('20120813182403'); - INSERT INTO schema_migrations (version) VALUES ('20120814144247'); INSERT INTO schema_migrations (version) VALUES ('20120814205636'); @@ -2127,8 +2149,6 @@ INSERT INTO schema_migrations (version) VALUES ('20121002194512'); INSERT INTO schema_migrations (version) VALUES ('20121003182142'); -INSERT INTO schema_migrations (version) VALUES ('20121004214008'); - INSERT INTO schema_migrations (version) VALUES ('20121022164546'); INSERT INTO schema_migrations (version) VALUES ('20121025143023'); @@ -2193,4 +2213,6 @@ INSERT INTO schema_migrations (version) VALUES ('20130722204142'); INSERT INTO schema_migrations (version) VALUES ('20130726021512'); -INSERT INTO schema_migrations (version) VALUES ('20130807201238'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20130807201238'); + +INSERT INTO schema_migrations (version) VALUES ('20130807232909'); \ No newline at end of file diff --git a/spec/models/app_settings_spec.rb b/spec/models/app_settings_spec.rb new file mode 100644 index 00000000..fc0d701a --- /dev/null +++ b/spec/models/app_settings_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe AppSettings do + + let(:instance) { AppSettings.instance } + + context "as a singleton" do + context "won't create a new instance" do + before { AppSettings.create } + specify { expect { AppSettings.create }.to raise_error } + end + + it "creates an instance as necessary" do + expect(AppSettings.count).to eq(0) + AppSettings.instance + expect(AppSettings.count).to eq(1) + end + end + + context "retrieving items from the singleton" do + context "creates an instance if none exists, and returns nil" do + specify { expect(AppSettings["foo"]).to be_nil } + end + + context "retrieves the underlying value if it exists" do + before { instance.data["foo"] = "bar"; instance.save } + specify { expect(AppSettings["foo"]).to eq("bar") } + end + end + + context "setting items in the singleton" do + context "creates an instance if none exists, and returns the setting" do + before { AppSettings["foo"] = "bar" } + specify { expect(instance.data["foo"]).to eq("bar") } + end + end + +end