calagator 0.0.1.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +23 -0
- data/README.md +75 -0
- data/Rakefile +12 -0
- data/app/assets/images/confused-alligator-sm.png +0 -0
- data/app/assets/images/disk.png +0 -0
- data/app/assets/images/edit.png +0 -0
- data/app/assets/images/external_sites/epdx.png +0 -0
- data/app/assets/images/external_sites/external.gif +0 -0
- data/app/assets/images/external_sites/facebook.png +0 -0
- data/app/assets/images/external_sites/foursquare.png +0 -0
- data/app/assets/images/external_sites/gowalla.png +0 -0
- data/app/assets/images/external_sites/lanyrd.png +0 -0
- data/app/assets/images/external_sites/meetup.png +0 -0
- data/app/assets/images/external_sites/plancast.png +0 -0
- data/app/assets/images/external_sites/shizzow.png +0 -0
- data/app/assets/images/external_sites/upcoming.png +0 -0
- data/app/assets/images/external_sites/yelp.png +0 -0
- data/app/assets/images/feed.png +0 -0
- data/app/assets/images/heart.png +0 -0
- data/app/assets/images/icon_ical.gif +0 -0
- data/app/assets/images/information.png +0 -0
- data/app/assets/images/nav_marker.gif +0 -0
- data/app/assets/images/nav_marker.png +0 -0
- data/app/assets/images/plus.png +0 -0
- data/app/assets/images/redx.png +0 -0
- data/app/assets/images/site-icon.png +0 -0
- data/app/assets/images/spinner.gif +0 -0
- data/app/assets/images/star.png +0 -0
- data/app/assets/images/subnav_marker.gif +0 -0
- data/app/assets/images/subnav_marker.png +0 -0
- data/app/assets/images/tag_blue.png +0 -0
- data/app/assets/images/tag_icons/angular.png +0 -0
- data/app/assets/images/tag_icons/beer.png +0 -0
- data/app/assets/images/tag_icons/bitcoin.png +0 -0
- data/app/assets/images/tag_icons/free.png +0 -0
- data/app/assets/images/tag_icons/golang.png +0 -0
- data/app/assets/images/tag_icons/html.png +0 -0
- data/app/assets/images/tag_icons/javascript.png +0 -0
- data/app/assets/images/tag_icons/linux.png +0 -0
- data/app/assets/images/tag_icons/php.png +0 -0
- data/app/assets/images/tag_icons/pizza.png +0 -0
- data/app/assets/images/tag_icons/python.png +0 -0
- data/app/assets/images/tag_icons/rails.png +0 -0
- data/app/assets/images/tag_icons/ruby.png +0 -0
- data/app/assets/images/tag_icons/sass.png +0 -0
- data/app/assets/images/tag_icons/swift.png +0 -0
- data/app/assets/images/transmit_blue.png +0 -0
- data/app/assets/images/weekday_background.gif +0 -0
- data/app/assets/javascripts/calagator.js +18 -0
- data/app/assets/javascripts/calagator/forms.js +60 -0
- data/app/assets/stylesheets/calagator.scss +8 -0
- data/app/assets/stylesheets/calagator/common.scss +14 -0
- data/app/assets/stylesheets/calagator/datepicker.scss +177 -0
- data/app/assets/stylesheets/calagator/errors.css +110 -0
- data/app/assets/stylesheets/calagator/forms.scss +4 -0
- data/app/assets/stylesheets/calagator/reset.css +40 -0
- data/app/assets/stylesheets/calagator/theme.css +0 -0
- data/app/controllers/calagator/admin_controller.rb +27 -0
- data/app/controllers/calagator/application_controller.rb +82 -0
- data/app/controllers/calagator/events_controller.rb +171 -0
- data/app/controllers/calagator/site_controller.rb +35 -0
- data/app/controllers/calagator/sources_controller.rb +94 -0
- data/app/controllers/calagator/venues_controller.rb +133 -0
- data/app/controllers/calagator/versions_controller.rb +20 -0
- data/app/helpers/calagator/application_helper.rb +128 -0
- data/app/helpers/calagator/events_helper.rb +184 -0
- data/app/helpers/calagator/google_event_export_helper.rb +67 -0
- data/app/helpers/calagator/mapping_helper.rb +103 -0
- data/app/helpers/calagator/sources_helper.rb +10 -0
- data/app/helpers/calagator/tags_helper.rb +42 -0
- data/app/helpers/calagator/time_range_helper.rb +166 -0
- data/app/models/calagator/event.rb +246 -0
- data/app/models/calagator/event/cloner.rb +39 -0
- data/app/models/calagator/event/ical_renderer.rb +155 -0
- data/app/models/calagator/event/overview.rb +45 -0
- data/app/models/calagator/event/saver.rb +58 -0
- data/app/models/calagator/event/search.rb +72 -0
- data/app/models/calagator/event/search_engine.rb +28 -0
- data/app/models/calagator/event/search_engine/apache_sunspot.rb +106 -0
- data/app/models/calagator/event/search_engine/sql.rb +107 -0
- data/app/models/calagator/source.rb +115 -0
- data/app/models/calagator/source/importer.rb +46 -0
- data/app/models/calagator/source/parser.rb +128 -0
- data/app/models/calagator/source/parser/facebook.rb +67 -0
- data/app/models/calagator/source/parser/hcal.rb +108 -0
- data/app/models/calagator/source/parser/http_authentication_required_error.rb +6 -0
- data/app/models/calagator/source/parser/ical.rb +186 -0
- data/app/models/calagator/source/parser/meetup.rb +72 -0
- data/app/models/calagator/source/parser/not_found.rb +9 -0
- data/app/models/calagator/source/parser/plancast.rb +59 -0
- data/app/models/calagator/venue.rb +161 -0
- data/app/models/calagator/venue/geocoder.rb +51 -0
- data/app/models/calagator/venue/search.rb +63 -0
- data/app/models/calagator/venue/search_engine.rb +24 -0
- data/app/models/calagator/venue/search_engine/apache_sunspot.rb +85 -0
- data/app/models/calagator/venue/search_engine/sql.rb +68 -0
- data/app/models/event/search_engine/base.rb +0 -0
- data/app/observers/calagator/cache_observer.rb +37 -0
- data/app/views/calagator/admin/events.html.erb +28 -0
- data/app/views/calagator/admin/index.html.erb +8 -0
- data/app/views/calagator/events/_feed_item.html.erb +62 -0
- data/app/views/calagator/events/_form.html.erb +63 -0
- data/app/views/calagator/events/_gcal_reminder.html.erb +1 -0
- data/app/views/calagator/events/_hcal.html.erb +41 -0
- data/app/views/calagator/events/_item.html.erb +120 -0
- data/app/views/calagator/events/_list.html.erb +30 -0
- data/app/views/calagator/events/_list_item.html.erb +37 -0
- data/app/views/calagator/events/_search_section.html.erb +15 -0
- data/app/views/calagator/events/_subnav.html.erb +13 -0
- data/app/views/calagator/events/_table.html.erb +74 -0
- data/app/views/calagator/events/duplicates.html.erb +58 -0
- data/app/views/calagator/events/edit.html.erb +3 -0
- data/app/views/calagator/events/index.atom.builder +32 -0
- data/app/views/calagator/events/index.html.erb +51 -0
- data/app/views/calagator/events/index.kml.erb +20 -0
- data/app/views/calagator/events/new.html.erb +7 -0
- data/app/views/calagator/events/search.html.erb +25 -0
- data/app/views/calagator/events/show.html.erb +81 -0
- data/app/views/calagator/site/_appropriateness_message.html.erb +15 -0
- data/app/views/calagator/site/_description.html.erb +3 -0
- data/app/views/calagator/site/about.html.erb +7 -0
- data/app/views/calagator/site/defunct.html.erb +15 -0
- data/app/views/calagator/site/export.html.erb +4 -0
- data/app/views/calagator/site/index.html.erb +40 -0
- data/app/views/calagator/site/opensearch.xml.builder +8 -0
- data/app/views/calagator/sources/_subnav.html.erb +0 -0
- data/app/views/calagator/sources/edit.html.erb +12 -0
- data/app/views/calagator/sources/import.html.erb +10 -0
- data/app/views/calagator/sources/index.html.erb +22 -0
- data/app/views/calagator/sources/new.html.erb +32 -0
- data/app/views/calagator/sources/show.html.erb +16 -0
- data/app/views/calagator/venues/_form.html.erb +57 -0
- data/app/views/calagator/venues/_subnav.html.erb +10 -0
- data/app/views/calagator/venues/duplicates.html.erb +65 -0
- data/app/views/calagator/venues/edit.html.erb +3 -0
- data/app/views/calagator/venues/index.html.erb +93 -0
- data/app/views/calagator/venues/index.kml.erb +15 -0
- data/app/views/calagator/venues/map.html.erb +4 -0
- data/app/views/calagator/venues/new.html.erb +5 -0
- data/app/views/calagator/venues/show.html.erb +113 -0
- data/app/views/calagator/versions/_chooser.html.erb +28 -0
- data/app/views/calagator/versions/_edit_with_chooser.html.erb +16 -0
- data/app/views/layouts/calagator/application.html.erb +110 -0
- data/config/deploy/local.rb +37 -0
- data/config/deploy/lucca.rb +33 -0
- data/config/initializers/dates.rb +7 -0
- data/config/initializers/formtastic.rb +82 -0
- data/config/initializers/geokit.rb +74 -0
- data/config/initializers/ics_renderer.rb +6 -0
- data/config/initializers/load_tag_model_extensions.rb +3 -0
- data/config/initializers/mime_types.rb +9 -0
- data/config/initializers/observers.rb +1 -0
- data/config/initializers/search_engine.rb +4 -0
- data/config/initializers/set_default_url_host.rb +5 -0
- data/config/initializers/time_get_zone.rb +8 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +55 -0
- data/config/secrets.yml.blag +75 -0
- data/config/sunspot.yml +23 -0
- data/db/development.sqlite3 +0 -0
- data/db/development.sqlite3.bak +0 -0
- data/db/development.sqlite3.old +0 -0
- data/db/development~20111112@110950.sqlite3 +0 -0
- data/db/migrate/001_create_events.rb +17 -0
- data/db/migrate/002_create_venues.rb +17 -0
- data/db/migrate/003_create_sources.rb +16 -0
- data/db/migrate/004_add_detailed_fields_to_venue.rb +19 -0
- data/db/migrate/005_add_end_time_to_events.rb +9 -0
- data/db/migrate/006_add_source_id_to_events.rb +9 -0
- data/db/migrate/008_add_source_id_to_venues.rb +10 -0
- data/db/migrate/009_add_duplicate_of_column_to_venues.rb +9 -0
- data/db/migrate/010_add_duplicate_of_column_to_events.rb +9 -0
- data/db/migrate/011_change_lat_long_type.rb +12 -0
- data/db/migrate/012_add_source_reimport.rb +9 -0
- data/db/migrate/013_change_end_time_to_duration.rb +11 -0
- data/db/migrate/014_remove_format_type_from_source.rb +9 -0
- data/db/migrate/015_create_updates.rb +15 -0
- data/db/migrate/016_remove_next_update_from_source.rb +9 -0
- data/db/migrate/20080705163959_change_duration_to_end_time.rb +11 -0
- data/db/migrate/20080705164959_create_tags_and_taggings.rb +28 -0
- data/db/migrate/20081011181519_create_versioned_events.rb +25 -0
- data/db/migrate/20081011193124_create_versioned_venues.rb +32 -0
- data/db/migrate/20081115190515_add_rrule_to_events.rb +15 -0
- data/db/migrate/20090912082129_create_versions.rb +18 -0
- data/db/migrate/20110219205156_add_closed_flag_to_venues.rb +9 -0
- data/db/migrate/20110220001008_add_wifi_flag_to_venues.rb +9 -0
- data/db/migrate/20110220011427_add_access_notes_to_venues.rb +9 -0
- data/db/migrate/20110220031117_add_events_count_to_venues.rb +8 -0
- data/db/migrate/20110604174521_add_venue_details_to_events.rb +9 -0
- data/db/migrate/20110717231316_acts_as_taggable_on_migration.rb +50 -0
- data/db/migrate/20120709092821_cleanup.rb +14 -0
- data/db/migrate/20120831234448_specify_venues_latitude_and_longitude_precision.rb +11 -0
- data/db/migrate/20150206085809_remove_updates.rb +13 -0
- data/db/migrate/20150207231355_add_locked_status_to_events.rb +5 -0
- data/db/production.sqlite3 +0 -0
- data/db/schema.rb +102 -0
- data/db/seeds.rb +16 -0
- data/db/test.sqlite3 +0 -0
- data/db/test2.sqlite3 +0 -0
- data/lib/calagator.rb +30 -0
- data/lib/calagator/blacklist_validator.rb +69 -0
- data/lib/calagator/decode_html_entities_hack.rb +33 -0
- data/lib/calagator/duplicate_checking.rb +133 -0
- data/lib/calagator/duplicate_checking/controller_actions.rb +38 -0
- data/lib/calagator/duplicate_checking/duplicate_finder.rb +83 -0
- data/lib/calagator/duplicate_checking/duplicate_squasher.rb +74 -0
- data/lib/calagator/engine.rb +30 -0
- data/lib/calagator/strip_whitespace.rb +19 -0
- data/lib/calagator/tag_model_extensions.rb +104 -0
- data/lib/calagator/url_prefixer.rb +9 -0
- data/lib/calagator/version.rb +3 -0
- data/lib/generators/calagator/install_generator.rb +39 -0
- data/lib/generators/calagator/templates/config/calagator.rb +26 -0
- data/lib/generators/calagator/templates/config/secrets.yml.sample +83 -0
- data/lib/secrets_reader.rb +76 -0
- data/lib/tasks/spec_db.rake +53 -0
- data/lib/tasks/sunspot_reindex_calagator.rake +6 -0
- data/lib/tasks/sunspot_solr_restart_enhancements.rake +19 -0
- data/lib/tasks/update_counter_caches.rake +13 -0
- data/lib/templates/erb/scaffold/_form.html.erb +11 -0
- data/lib/theme_reader.rb +17 -0
- data/lib/wait_for_solr.rb +25 -0
- data/spec/controllers/calagator/application_controller_spec.rb +47 -0
- data/spec/controllers/calagator/events_controller_spec.rb +794 -0
- data/spec/controllers/calagator/site_controller_spec.rb +59 -0
- data/spec/controllers/calagator/sources_controller_spec.rb +439 -0
- data/spec/controllers/calagator/venues_controller_spec.rb +319 -0
- data/spec/controllers/calagator/versions_controller_spec.rb +82 -0
- data/spec/controllers/squash_many_duplicates_examples.rb +49 -0
- data/spec/dummy/Gemfile +39 -0
- data/spec/dummy/Gemfile.lock +195 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/images/rails.png +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +16 -0
- data/spec/dummy/app/assets/stylesheets/application.css +14 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +62 -0
- data/spec/dummy/config/boot.rb +6 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/calagator.rb +26 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +59 -0
- data/spec/dummy/config/secrets.yml +83 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20150309023304_create_events.calagator.rb +18 -0
- data/spec/dummy/db/migrate/20150309023305_create_venues.calagator.rb +18 -0
- data/spec/dummy/db/migrate/20150309023306_create_sources.calagator.rb +17 -0
- data/spec/dummy/db/migrate/20150309023307_add_detailed_fields_to_venue.calagator.rb +20 -0
- data/spec/dummy/db/migrate/20150309023308_add_end_time_to_events.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023309_add_source_id_to_events.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023310_add_source_id_to_venues.calagator.rb +11 -0
- data/spec/dummy/db/migrate/20150309023311_add_duplicate_of_column_to_venues.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023312_add_duplicate_of_column_to_events.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023313_change_lat_long_type.calagator.rb +13 -0
- data/spec/dummy/db/migrate/20150309023314_add_source_reimport.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023315_change_end_time_to_duration.calagator.rb +12 -0
- data/spec/dummy/db/migrate/20150309023316_remove_format_type_from_source.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023317_create_updates.calagator.rb +16 -0
- data/spec/dummy/db/migrate/20150309023318_remove_next_update_from_source.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023319_change_duration_to_end_time.calagator.rb +12 -0
- data/spec/dummy/db/migrate/20150309023320_create_tags_and_taggings.calagator.rb +29 -0
- data/spec/dummy/db/migrate/20150309023321_create_versioned_events.calagator.rb +26 -0
- data/spec/dummy/db/migrate/20150309023322_create_versioned_venues.calagator.rb +33 -0
- data/spec/dummy/db/migrate/20150309023323_add_rrule_to_events.calagator.rb +16 -0
- data/spec/dummy/db/migrate/20150309023324_create_versions.calagator.rb +19 -0
- data/spec/dummy/db/migrate/20150309023325_add_closed_flag_to_venues.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023326_add_wifi_flag_to_venues.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023327_add_access_notes_to_venues.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023328_add_events_count_to_venues.calagator.rb +9 -0
- data/spec/dummy/db/migrate/20150309023329_add_venue_details_to_events.calagator.rb +10 -0
- data/spec/dummy/db/migrate/20150309023330_acts_as_taggable_on_migration.calagator.rb +51 -0
- data/spec/dummy/db/migrate/20150309023331_cleanup.calagator.rb +15 -0
- data/spec/dummy/db/migrate/20150309023332_specify_venues_latitude_and_longitude_precision.calagator.rb +12 -0
- data/spec/dummy/db/migrate/20150309023333_remove_updates.calagator.rb +14 -0
- data/spec/dummy/db/migrate/20150309023334_add_locked_status_to_events.calagator.rb +6 -0
- data/spec/dummy/db/schema.rb +95 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/doc/README_FOR_APP +2 -0
- data/spec/dummy/log/development.log +273 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/test/performance/browsing_test.rb +12 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/factories.rb +93 -0
- data/spec/features/add_event_spec.rb +99 -0
- data/spec/features/add_venue_spec.rb +34 -0
- data/spec/features/admin_auth_spec.rb +22 -0
- data/spec/features/admin_lock_event_spec.rb +41 -0
- data/spec/features/import_events_from_feed_spec.rb +43 -0
- data/spec/features/managing_event_spec.rb +111 -0
- data/spec/features/managing_venue_spec.rb +71 -0
- data/spec/features/search_event_spec.rb +27 -0
- data/spec/helpers/calagator/application_helper_spec.rb +82 -0
- data/spec/helpers/calagator/events_helper_spec.rb +172 -0
- data/spec/helpers/calagator/google_event_export_helper_spec.rb +70 -0
- data/spec/helpers/calagator/sources_helper_spec.rb +12 -0
- data/spec/helpers/calagator/tags_helper_spec.rb +85 -0
- data/spec/helpers/calagator/time_range_helper_spec.rb +59 -0
- data/spec/lib/calagator/blacklist_validator_spec.rb +65 -0
- data/spec/lib/calagator/decode_html_entities_hack_spec.rb +54 -0
- data/spec/lib/calagator/settings_spec.rb +20 -0
- data/spec/lib/calagator/url_prefixer_spec.rb +33 -0
- data/spec/lib/secrets_reader_spec.rb +65 -0
- data/spec/models/calagator/event/cloner_spec.rb +43 -0
- data/spec/models/calagator/event/overview_spec.rb +79 -0
- data/spec/models/calagator/event/search_spec.rb +103 -0
- data/spec/models/calagator/event_search_spec.rb +149 -0
- data/spec/models/calagator/event_spec.rb +859 -0
- data/spec/models/calagator/source/parser_facebook_spec.rb +73 -0
- data/spec/models/calagator/source/parser_hcal_spec.rb +69 -0
- data/spec/models/calagator/source/parser_ical_non_standard_spec.rb +91 -0
- data/spec/models/calagator/source/parser_ical_spec.rb +322 -0
- data/spec/models/calagator/source/parser_meetup_spec.rb +69 -0
- data/spec/models/calagator/source/parser_plancast_spec.rb +53 -0
- data/spec/models/calagator/source/parser_spec.rb +238 -0
- data/spec/models/calagator/source_spec.rb +135 -0
- data/spec/models/calagator/venue/search_spec.rb +92 -0
- data/spec/models/calagator/venue_search_spec.rb +124 -0
- data/spec/models/calagator/venue_spec.rb +346 -0
- data/spec/models/tag_spec.rb +35 -0
- data/spec/rails_helper.rb +39 -0
- data/spec/spec_helper.rb +140 -0
- data/spec/support/disable_geocoding.rb +1 -0
- data/spec/support/http_samples.rb +5 -0
- data/spec/support/samples/facebook.json +23 -0
- data/spec/support/samples/hcal_basic.xml +6 -0
- data/spec/support/samples/hcal_dup_event_dup_venue.xml +13 -0
- data/spec/support/samples/hcal_event_duplicates_fixture.xml +13 -0
- data/spec/support/samples/hcal_event_wo_lat_and_long.xml +14 -0
- data/spec/support/samples/hcal_multiple.xml +16 -0
- data/spec/support/samples/hcal_same_event_twice_with_different_venues.xml +12 -0
- data/spec/support/samples/hcal_single.xml +8 -0
- data/spec/support/samples/hcal_two_identical_events.xml +14 -0
- data/spec/support/samples/hcal_upcoming_v1.html +412 -0
- data/spec/support/samples/hcal_upcoming_v2.html +749 -0
- data/spec/support/samples/hcal_upcoming_v3.html +685 -0
- data/spec/support/samples/hcal_upcoming_v4.html +761 -0
- data/spec/support/samples/ical_apple.ics +22 -0
- data/spec/support/samples/ical_apple_v3.ics +37 -0
- data/spec/support/samples/ical_basic.ics +15 -0
- data/spec/support/samples/ical_basic_with_duration.ics +16 -0
- data/spec/support/samples/ical_event_with_squashed_venue.ics +12 -0
- data/spec/support/samples/ical_eventful_many.ics +504 -0
- data/spec/support/samples/ical_gmt.ics +12 -0
- data/spec/support/samples/ical_google.ics +786 -0
- data/spec/support/samples/ical_multiple_calendars.ics +111 -0
- data/spec/support/samples/ical_upcoming.ics +36 -0
- data/spec/support/samples/ical_upcoming_many.ics +682 -0
- data/spec/support/samples/ical_upcoming_v2.ics +43 -0
- data/spec/support/samples/ical_z.ics +10 -0
- data/spec/support/samples/meetup.ics +16 -0
- data/spec/support/samples/meetup.json +31 -0
- data/spec/support/samples/plancast.ics +59 -0
- data/spec/support/samples/plancast.json +51 -0
- data/spec/support/samples/plancast_with_missing_venue.json +39 -0
- data/spec/support/samples/upcoming_v1.xml +8 -0
- data/spec/support/samples/upcoming_v2_with_invalid_utc_dates.xml +8 -0
- data/spec/support/time_convenience_methods.rb +8 -0
- data/spec/support/time_zones.rb +12 -0
- data/spec/support/url_helpers.rb +5 -0
- data/spec/support/wait_for_ajax.rb +15 -0
- data/spec/support/webmock.rb +8 -0
- data/spec/travis_spec.rb +7 -0
- metadata +1194 -0
data/db/seeds.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ruby encoding: utf-8
|
|
2
|
+
|
|
3
|
+
# This file should contain all the record creation needed to seed the database with its default values.
|
|
4
|
+
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
|
|
5
|
+
#
|
|
6
|
+
# Examples:
|
|
7
|
+
#
|
|
8
|
+
# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
|
|
9
|
+
# Mayor.create(:name => 'Daley', :city => cities.first)
|
|
10
|
+
|
|
11
|
+
require 'faker'
|
|
12
|
+
|
|
13
|
+
FactoryGirl.create_list(:seed_venue, 25, :with_events)
|
|
14
|
+
FactoryGirl.create_list(:seed_venue, 25)
|
|
15
|
+
FactoryGirl.create_list(:seed_event, 25, :with_venue)
|
|
16
|
+
FactoryGirl.create_list(:seed_event, 25)
|
data/db/test.sqlite3
ADDED
|
Binary file
|
data/db/test2.sqlite3
ADDED
|
Binary file
|
data/lib/calagator.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "calagator/engine"
|
|
2
|
+
|
|
3
|
+
require "formtastic"
|
|
4
|
+
require "rails_autolink"
|
|
5
|
+
require "nokogiri"
|
|
6
|
+
require "columnize"
|
|
7
|
+
require "geokit"
|
|
8
|
+
require "htmlentities"
|
|
9
|
+
require "paper_trail"
|
|
10
|
+
require "ri_cal"
|
|
11
|
+
require "will_paginate"
|
|
12
|
+
require "will_paginate/array"
|
|
13
|
+
require "rest-client"
|
|
14
|
+
require "loofah"
|
|
15
|
+
require "loofah-activerecord"
|
|
16
|
+
require "bluecloth"
|
|
17
|
+
require "formtastic"
|
|
18
|
+
require "acts-as-taggable-on"
|
|
19
|
+
require "jquery-rails"
|
|
20
|
+
require "jquery-ui-rails"
|
|
21
|
+
require "progress_bar"
|
|
22
|
+
require "font-awesome-rails"
|
|
23
|
+
require "paper_trail_manager"
|
|
24
|
+
require "utf8-cleaner"
|
|
25
|
+
require "sunspot_rails"
|
|
26
|
+
require "sunspot_solr"
|
|
27
|
+
require "lucene_query"
|
|
28
|
+
|
|
29
|
+
module Calagator
|
|
30
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# = BlacklistValidator
|
|
2
|
+
#
|
|
3
|
+
# A naively simple mixin that blacklists content in ActiveModel objects.
|
|
4
|
+
#
|
|
5
|
+
# == Usage
|
|
6
|
+
#
|
|
7
|
+
# Let's say that your applications lets people post messages, but don't want
|
|
8
|
+
# them using the word "viagra" in posts as a naively simple way of preventing
|
|
9
|
+
# spam.
|
|
10
|
+
#
|
|
11
|
+
# You'd first create a config/blacklist.txt file with a line like:
|
|
12
|
+
# \bviagrab\b
|
|
13
|
+
#
|
|
14
|
+
# And then you'd include the blacklisting feature into your Message class like:
|
|
15
|
+
#
|
|
16
|
+
# class Message < ActiveRecord::Base
|
|
17
|
+
# validates :title, :content, blacklist: true
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Now including the word "viagra" in your record's values will fail:
|
|
21
|
+
#
|
|
22
|
+
# message = Message.new(title: "foo viagra bar")
|
|
23
|
+
# message.valid? # => false
|
|
24
|
+
#
|
|
25
|
+
# Available validator options:
|
|
26
|
+
# * patterns: Array of regular expressions that will be matched
|
|
27
|
+
# against the given attribute contents and any matches will cause the
|
|
28
|
+
# record to be marked invalid.
|
|
29
|
+
# * blacklist: Reads an array of blacklisted regular expressions from
|
|
30
|
+
# a filename.
|
|
31
|
+
# * message: Error message to use on invalid records.
|
|
32
|
+
#
|
|
33
|
+
# If no :patterns or :blacklist is given, patterns are read from:
|
|
34
|
+
# * config/blacklist.txt
|
|
35
|
+
# * config/blacklist-local.txt
|
|
36
|
+
|
|
37
|
+
class BlacklistValidator < ActiveModel::EachValidator
|
|
38
|
+
BLACKLIST_DEFAULT_MESSAGE = "contains blacklisted content"
|
|
39
|
+
|
|
40
|
+
def validate_each(record, attribute, value)
|
|
41
|
+
if value.present? && patterns.any? { |pattern| value.match(pattern) }
|
|
42
|
+
record.errors.add attribute, message
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def message
|
|
49
|
+
options.fetch(:message, BLACKLIST_DEFAULT_MESSAGE)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def patterns
|
|
53
|
+
@patterns ||= options.fetch(:patterns) do
|
|
54
|
+
[
|
|
55
|
+
Calagator.blacklist_patterns,
|
|
56
|
+
get_blacklist_patterns_from(options.fetch(:blacklist, "blacklist.txt")),
|
|
57
|
+
].flatten.compact
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get_blacklist_patterns_from(filename)
|
|
62
|
+
filename = Rails.root.join('config',filename) unless filename.match(/[\/\\]/)
|
|
63
|
+
return unless File.exists?(filename)
|
|
64
|
+
|
|
65
|
+
File.readlines(filename).map do |line|
|
|
66
|
+
Regexp.new(line.strip, Regexp::IGNORECASE)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# = DecodeHtmlEntitiesHack
|
|
2
|
+
#
|
|
3
|
+
# Loofah encodes HTML entities in every string that gets passed through it.
|
|
4
|
+
# This includes things like the friendly ampersand which we don't want to be HTML
|
|
5
|
+
# encoded in our database, so we need to decode the things that it changes.
|
|
6
|
+
#
|
|
7
|
+
# This should be included in models that use xss_foliate somewhere _after_ the xss_folidate call.
|
|
8
|
+
#
|
|
9
|
+
# This is, as the name of the module suggests, a giant hack. At some point, it should
|
|
10
|
+
# be removed after the issue is resolved in the underlying library:
|
|
11
|
+
# https://github.com/flavorjones/loofah/issues/20#issuecomment-1751538
|
|
12
|
+
#
|
|
13
|
+
# Warning: this effectively renders loofah's "escape" scrubbing mode useless by
|
|
14
|
+
# undoing everything it does. Don't use that mode.
|
|
15
|
+
#
|
|
16
|
+
module Calagator
|
|
17
|
+
|
|
18
|
+
module DecodeHtmlEntitiesHack
|
|
19
|
+
def self.included(base)
|
|
20
|
+
base.set_callback(:validate, :before, :decode_html_entities)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def decode_html_entities
|
|
24
|
+
self.attributes.each do |field, value|
|
|
25
|
+
decoded_content = HTMLEntities.new.decode(value)
|
|
26
|
+
if decoded_content.present? && !(decoded_content == value)
|
|
27
|
+
self.send("#{field}=", decoded_content)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# = DuplicateChecking
|
|
2
|
+
#
|
|
3
|
+
# This mixin provides a way for ActiveRecord classes to find and squash duplicates.
|
|
4
|
+
#
|
|
5
|
+
# Example:
|
|
6
|
+
#
|
|
7
|
+
# # Define your class
|
|
8
|
+
# class Thing < ActiveRecord::Base
|
|
9
|
+
# # Load the mixin into your class
|
|
10
|
+
# include DuplicateChecking
|
|
11
|
+
#
|
|
12
|
+
# # Declare attributes that should be ignored during duplicate checks
|
|
13
|
+
# duplicate_checking_ignores_attributes :random_value
|
|
14
|
+
#
|
|
15
|
+
# # Declare associations that should be ignored during duplicate squashing
|
|
16
|
+
# duplicate_squashing_ignores_associations :tags
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # Set duplicates on objects
|
|
20
|
+
# foo1 = Thing.create!!:name => "foo")
|
|
21
|
+
# foo2 = Thing.create!(:name => "foo", :duplicate_of => foo1)
|
|
22
|
+
# bar = Thing.create!(:name => "bar")
|
|
23
|
+
#
|
|
24
|
+
# # Check whether record is set as duplicate
|
|
25
|
+
# foo1.duplicate? # => false
|
|
26
|
+
# foo2.duplicate? # => true
|
|
27
|
+
# bar.duplicate? # => false
|
|
28
|
+
#
|
|
29
|
+
# # Find duplicate of a record
|
|
30
|
+
# foo3.find_exact_duplicates # => [foo1, foo2]
|
|
31
|
+
# bar.find_exact_duplicates # => nil
|
|
32
|
+
require "calagator/duplicate_checking/duplicate_finder"
|
|
33
|
+
require "calagator/duplicate_checking/duplicate_squasher"
|
|
34
|
+
|
|
35
|
+
module Calagator
|
|
36
|
+
|
|
37
|
+
module DuplicateChecking
|
|
38
|
+
DUPLICATE_MARK_COLUMN = :duplicate_of_id
|
|
39
|
+
DEFAULT_SQUASH_METHOD = :mark
|
|
40
|
+
DUPLICATE_CHECKING_IGNORES_ATTRIBUTES =
|
|
41
|
+
Set.new((%w(created_at updated_at id) + [DUPLICATE_MARK_COLUMN]).map(&:to_sym))
|
|
42
|
+
|
|
43
|
+
def self.included(base)
|
|
44
|
+
base.extend ClassMethods
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def duplicate?
|
|
48
|
+
duplicate_of.present?
|
|
49
|
+
end
|
|
50
|
+
alias_method :marked_as_duplicate?, :duplicate?
|
|
51
|
+
alias_method :slave?, :duplicate?
|
|
52
|
+
|
|
53
|
+
def master?
|
|
54
|
+
!slave?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Return the ultimate master for a record, which may be the record itself.
|
|
58
|
+
def progenitor
|
|
59
|
+
parent = self
|
|
60
|
+
seen = Set.new
|
|
61
|
+
|
|
62
|
+
loop do
|
|
63
|
+
return parent if parent.master?
|
|
64
|
+
raise DuplicateCheckingError, "Loop detected in duplicates chain at #{parent.class}##{parent.id}" if seen.include?(parent)
|
|
65
|
+
seen << parent
|
|
66
|
+
parent = parent.duplicate_of
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Return either an Array of exact duplicates for this record, or nil if no exact duplicates were found.
|
|
71
|
+
#
|
|
72
|
+
# Note that this method requires that all associations are set before this method is called.
|
|
73
|
+
def find_exact_duplicates
|
|
74
|
+
matchable_attributes = attributes.reject { |key, value|
|
|
75
|
+
self.class.duplicate_checking_ignores_attributes.include?(key.to_sym)
|
|
76
|
+
}
|
|
77
|
+
duplicates = self.class.where(matchable_attributes).reject{|t| t.id == id}
|
|
78
|
+
duplicates.blank? ? nil : duplicates
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
module ClassMethods
|
|
82
|
+
def self.extended(klass)
|
|
83
|
+
klass.instance_eval do
|
|
84
|
+
cattr_accessor(:_duplicate_checking_ignores_attributes) { Set.new }
|
|
85
|
+
cattr_accessor(:_duplicate_squashing_ignores_associations) { Set.new }
|
|
86
|
+
|
|
87
|
+
belongs_to :duplicate_of, :class_name => name, :foreign_key => DUPLICATE_MARK_COLUMN
|
|
88
|
+
has_many :duplicates, :class_name => name, :foreign_key => DUPLICATE_MARK_COLUMN
|
|
89
|
+
|
|
90
|
+
scope :marked_duplicates, -> { where("#{table_name}.#{DUPLICATE_MARK_COLUMN} IS NOT NULL") }
|
|
91
|
+
scope :non_duplicates, -> { where("#{table_name}.#{DUPLICATE_MARK_COLUMN} IS NULL") }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Return set of attributes that should be ignored for duplicate checking
|
|
96
|
+
def duplicate_checking_ignores_attributes(*args)
|
|
97
|
+
_duplicate_checking_ignores_attributes.merge(args.map(&:to_sym)) unless args.empty?
|
|
98
|
+
DUPLICATE_CHECKING_IGNORES_ATTRIBUTES + _duplicate_checking_ignores_attributes
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Return set of associations that will be ignored during duplicate squashing
|
|
102
|
+
def duplicate_squashing_ignores_associations(*args)
|
|
103
|
+
_duplicate_squashing_ignores_associations.merge(args.map(&:to_sym)) unless args.empty?
|
|
104
|
+
_duplicate_squashing_ignores_associations
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Return events with duplicate values for a given set of fields.
|
|
108
|
+
#
|
|
109
|
+
# Options:
|
|
110
|
+
# * :grouped => Return Hash of events grouped by commonality, rather than returning an Array. Defaults to false.
|
|
111
|
+
# * :where => String that specifies additional arguments to add to the WHERE clause.
|
|
112
|
+
# * :select => String that specified additional arguments to add to the SELECT clause.
|
|
113
|
+
# * :from => String that specifies additional arguments to add to the FROM clause
|
|
114
|
+
# * :joins => String that specifies additional argument to add to a JOINS clause.
|
|
115
|
+
def find_duplicates_by(fields, options={})
|
|
116
|
+
DuplicateFinder.new(self, fields, options).find
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Squash duplicates. Options accept ActiveRecord instances or IDs.
|
|
120
|
+
#
|
|
121
|
+
# Options:
|
|
122
|
+
# :duplicates => ActiveRecord instance(s) to mark as duplicates
|
|
123
|
+
# :master => ActiveRecord instance to use as master
|
|
124
|
+
def squash(master, duplicates)
|
|
125
|
+
DuplicateSquasher.new(master, duplicates, name.downcase).squash
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class DuplicateCheckingError < Exception
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Calagator
|
|
2
|
+
|
|
3
|
+
module DuplicateChecking
|
|
4
|
+
module ControllerActions
|
|
5
|
+
# GET /#{model_class}/duplicates
|
|
6
|
+
def duplicates
|
|
7
|
+
@type = params[:type]
|
|
8
|
+
@grouped_venues = @grouped_events = model_class.find_duplicates_by_type(@type)
|
|
9
|
+
rescue ArgumentError => e
|
|
10
|
+
@grouped_venues = @grouped_events = {}
|
|
11
|
+
flash[:failure] = e.to_s
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# POST /#{model_class}/squash_multiple_duplicates
|
|
15
|
+
def squash_many_duplicates
|
|
16
|
+
master = model_class.find_by_id(params[:master_id])
|
|
17
|
+
duplicate_ids = params.keys.grep(/^duplicate_id_\d+$/){|t| params[t].to_i}
|
|
18
|
+
duplicates = model_class.where(id: duplicate_ids)
|
|
19
|
+
|
|
20
|
+
squasher = model_class.squash(master, duplicates)
|
|
21
|
+
if squasher.success
|
|
22
|
+
flash[:success] = squasher.success
|
|
23
|
+
else
|
|
24
|
+
flash[:failure] = squasher.failure
|
|
25
|
+
end
|
|
26
|
+
redirect_to action: "duplicates", type: params[:type]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def model_class
|
|
32
|
+
# Derive model class from controller name
|
|
33
|
+
"Calagator::#{controller_name.singularize.titleize}".constantize
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Calagator
|
|
2
|
+
|
|
3
|
+
module DuplicateChecking
|
|
4
|
+
class DuplicateFinder < Struct.new(:model, :fields, :options)
|
|
5
|
+
def find
|
|
6
|
+
scope = model.select("a.*")
|
|
7
|
+
scope = scope.from("#{model.table_name} a, #{model.table_name} b")
|
|
8
|
+
scope = scope.where(options[:where]) if options[:where]
|
|
9
|
+
scope = scope.where("a.id <> b.id")
|
|
10
|
+
scope = scope.where("a.duplicate_of_id" => nil)
|
|
11
|
+
scope = scope.where(query)
|
|
12
|
+
scope.distinct
|
|
13
|
+
|
|
14
|
+
records = scope.all
|
|
15
|
+
records = group_by_fields(records) if grouped
|
|
16
|
+
records
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fields
|
|
20
|
+
super || :all
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def query
|
|
26
|
+
case fields
|
|
27
|
+
when :all then query_from_all
|
|
28
|
+
when :any then query_from_any
|
|
29
|
+
else query_from_fields
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def grouped
|
|
34
|
+
options[:grouped] || false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def group_by_fields records
|
|
38
|
+
# Group by the field values we're matching on; skip any values for which we only have one record
|
|
39
|
+
records = records.group_by do |record|
|
|
40
|
+
fields.map do |field|
|
|
41
|
+
record.read_attribute(field)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
records.reject { |value, group| group.size <= 1 }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def query_from_all
|
|
48
|
+
attributes.map do |attr|
|
|
49
|
+
"((a.#{attr} = b.#{attr}) OR (a.#{attr} IS NULL AND b.#{attr} IS NULL))"
|
|
50
|
+
end.join(" AND ")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def query_from_any
|
|
54
|
+
attributes.map do |attr|
|
|
55
|
+
query = "(a.#{attr} = b.#{attr} AND ("
|
|
56
|
+
column = model.columns.find {|column| column.name.to_sym == attr}
|
|
57
|
+
case column.type
|
|
58
|
+
when :integer, :decimal
|
|
59
|
+
query << "a.#{attr} != 0 AND "
|
|
60
|
+
when :string, :text
|
|
61
|
+
query << "a.#{attr} != '' AND "
|
|
62
|
+
end
|
|
63
|
+
query << "a.#{attr} IS NOT NULL))"
|
|
64
|
+
end.join(" OR ")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def query_from_fields
|
|
68
|
+
raise ArgumentError, "Unknown fields: #{fields.inspect}" if (Array(fields) - attributes).any?
|
|
69
|
+
Array(fields).map do |attr|
|
|
70
|
+
"a.#{attr} = b.#{attr}"
|
|
71
|
+
end.join(" AND ")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def attributes
|
|
75
|
+
# TODO make find_duplicates_by(:all) pay attention to ignore fields
|
|
76
|
+
model.new.attribute_names.map(&:to_sym).reject do |attr|
|
|
77
|
+
[:id, :created_at, :updated_at, :duplicate_of_id, :version].include?(attr)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Calagator
|
|
2
|
+
|
|
3
|
+
module DuplicateChecking
|
|
4
|
+
class DuplicateSquasher < Struct.new(:master, :duplicates, :model_name, :failure, :success)
|
|
5
|
+
def duplicates
|
|
6
|
+
Array(super)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def valid?
|
|
10
|
+
name = model_name.split("::").last
|
|
11
|
+
self.failure = "A master #{name} must be selected." if master.blank?
|
|
12
|
+
self.failure = "At least one duplicate #{name} must be selected." if duplicates.empty?
|
|
13
|
+
self.failure = "The master #{name} could not be squashed into itself." if duplicates.include?(master)
|
|
14
|
+
failure.blank?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def squash
|
|
18
|
+
if valid?
|
|
19
|
+
duplicates.each do |duplicate|
|
|
20
|
+
SingleSquasher.new(master, duplicate, model_name).squash
|
|
21
|
+
end
|
|
22
|
+
name = model_name.split("::").last
|
|
23
|
+
self.success = "Squashed duplicate #{name.pluralize} #{duplicates.map(&:title)} into master #{master.id}."
|
|
24
|
+
end
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class SingleSquasher < Struct.new(:master, :duplicate, :model_name)
|
|
29
|
+
def squash
|
|
30
|
+
# Transfer any venues that use this now duplicate venue as a master
|
|
31
|
+
if duplicate.duplicates.any?
|
|
32
|
+
DuplicateSquasher.new(master, duplicate.duplicates, model_name).squash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
squash_associations
|
|
36
|
+
|
|
37
|
+
duplicate.update_attribute(:duplicate_of, master)
|
|
38
|
+
duplicate
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def squash_associations
|
|
44
|
+
# Transfer any has_many associations of this model to the master
|
|
45
|
+
master.class.reflect_on_all_associations(:has_many).each do |association|
|
|
46
|
+
next if association.name == :duplicates
|
|
47
|
+
next if master.class.duplicate_squashing_ignores_associations.include?(association.name)
|
|
48
|
+
|
|
49
|
+
# Handle tags - can't simply reassign, need to be unique, and they may have some of the same tags
|
|
50
|
+
if association.name == :tag_taggings
|
|
51
|
+
squash_tags
|
|
52
|
+
else
|
|
53
|
+
squash_association association
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# custom behavior for tags, concatentate the two objects tag strings together
|
|
59
|
+
def squash_tags
|
|
60
|
+
master.tag_list = master.tag_list + duplicate.tag_list
|
|
61
|
+
master.save_tags
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def squash_association(association)
|
|
65
|
+
foreign_objects = duplicate.send(association.name)
|
|
66
|
+
foreign_objects.each do |object|
|
|
67
|
+
object.update_attribute(association.foreign_key, master.id)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|