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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Calagator
|
|
2
|
+
|
|
3
|
+
class Event < ActiveRecord::Base
|
|
4
|
+
class SearchEngine
|
|
5
|
+
class Sql < Struct.new(:query, :opts)
|
|
6
|
+
# Return an Array of non-duplicate Event instances matching the search +query+..
|
|
7
|
+
#
|
|
8
|
+
# Options:
|
|
9
|
+
# * :order => How to order the entries? Defaults to :date. Permitted values:
|
|
10
|
+
# * :date => Sort by date
|
|
11
|
+
# * :name => Sort by event title
|
|
12
|
+
# * :title => same as :name
|
|
13
|
+
# * :venue => Sort by venue title
|
|
14
|
+
# * :limit => Maximum number of entries to return. Defaults to 50.
|
|
15
|
+
# * :skip_old => Return old entries? Defaults to false.
|
|
16
|
+
def self.search(*args)
|
|
17
|
+
new(*args).all
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.score?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def all
|
|
25
|
+
current_events + past_events
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def current_events
|
|
31
|
+
search.current.scope.to_a
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def past_events
|
|
35
|
+
skip_old ? [] : search.past.scope.to_a
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def search
|
|
39
|
+
base.keywords.order.limit
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def skip_old
|
|
43
|
+
opts[:skip_old] == true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
attr_accessor :scope
|
|
49
|
+
|
|
50
|
+
def base
|
|
51
|
+
column_names = Event.column_names.map { |name| "events.#{name}"}
|
|
52
|
+
column_names << "venues.id"
|
|
53
|
+
@scope = Event.scoped
|
|
54
|
+
.group(column_names)
|
|
55
|
+
.joins("LEFT OUTER JOIN taggings on taggings.taggable_id = events.id AND taggings.taggable_type LIKE '%Event'")
|
|
56
|
+
.joins("LEFT OUTER JOIN tags ON tags.id = taggings.tag_id")
|
|
57
|
+
.includes(:venue)
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def keywords
|
|
62
|
+
query_conditions = @scope.where('LOWER(events.title) LIKE ?', "%#{query.downcase}%")
|
|
63
|
+
.where('LOWER(events.description) LIKE ?', "%#{query.downcase}%")
|
|
64
|
+
|
|
65
|
+
query_conditions = query.split.inject(query_conditions) do |query_conditions, keyword|
|
|
66
|
+
like = "%#{keyword.downcase}%"
|
|
67
|
+
query_conditions
|
|
68
|
+
.where(['LOWER(events.url) LIKE ?', like])
|
|
69
|
+
.where(['LOWER(tags.name) = ?', keyword])
|
|
70
|
+
end
|
|
71
|
+
@scope = @scope.where(query_conditions.where_values.join(' OR '))
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def order
|
|
76
|
+
order = case opts[:order].try(:to_sym)
|
|
77
|
+
when :name, :title
|
|
78
|
+
'LOWER(events.title) ASC'
|
|
79
|
+
when :location, :venue
|
|
80
|
+
'LOWER(venues.title) ASC'
|
|
81
|
+
else
|
|
82
|
+
'events.start_time DESC'
|
|
83
|
+
end
|
|
84
|
+
@scope = @scope.order(order)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def limit
|
|
89
|
+
limit = opts.fetch(:limit, 50)
|
|
90
|
+
@scope = @scope.limit(limit)
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def current
|
|
95
|
+
@scope = @scope.where("events.start_time >= ?", Date.yesterday.to_time)
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def past
|
|
100
|
+
@scope = @scope.where("events.start_time < ?", Date.yesterday.to_time)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# == Schema Information
|
|
2
|
+
# Schema version: 20110604174521
|
|
3
|
+
#
|
|
4
|
+
# Table name: sources
|
|
5
|
+
#
|
|
6
|
+
# id :integer not null, primary key
|
|
7
|
+
# title :string(255)
|
|
8
|
+
# url :string(255)
|
|
9
|
+
# imported_at :datetime
|
|
10
|
+
# created_at :datetime
|
|
11
|
+
# updated_at :datetime
|
|
12
|
+
# reimport :boolean
|
|
13
|
+
#
|
|
14
|
+
|
|
15
|
+
# == Source
|
|
16
|
+
#
|
|
17
|
+
# A model that represents a source of events data, such as feeds for hCal, iCal, etc.
|
|
18
|
+
require "calagator/decode_html_entities_hack"
|
|
19
|
+
require "paper_trail"
|
|
20
|
+
require "loofah-activerecord"
|
|
21
|
+
require "loofah/activerecord/xss_foliate"
|
|
22
|
+
|
|
23
|
+
module Calagator
|
|
24
|
+
|
|
25
|
+
class Source < ActiveRecord::Base
|
|
26
|
+
self.table_name = "sources"
|
|
27
|
+
|
|
28
|
+
attr_protected
|
|
29
|
+
|
|
30
|
+
validate :assert_url
|
|
31
|
+
|
|
32
|
+
has_many :events, :dependent => :destroy
|
|
33
|
+
has_many :venues, :dependent => :destroy
|
|
34
|
+
|
|
35
|
+
scope :listing, -> { order('created_at DESC') }
|
|
36
|
+
|
|
37
|
+
has_paper_trail
|
|
38
|
+
|
|
39
|
+
xss_foliate
|
|
40
|
+
include DecodeHtmlEntitiesHack
|
|
41
|
+
|
|
42
|
+
# Return a newly-created or existing Source record matching the given
|
|
43
|
+
# attributes. The +attrs+ hash is the same format as used when calling
|
|
44
|
+
# Source::new.
|
|
45
|
+
#
|
|
46
|
+
# This method is intended to supplement the import process by providing a
|
|
47
|
+
# single Source record for each unique URL, thus when multiple people import
|
|
48
|
+
# the same URL, there will only be one Source record.
|
|
49
|
+
#
|
|
50
|
+
# The :reimport flag is given special handling: If the original Source record
|
|
51
|
+
# has this set to true, it will never be set back to false by this method.
|
|
52
|
+
# The intent behind this is that if one person wants this Source reimported,
|
|
53
|
+
# the reimporting shouldn't be disabled by someone else manually importing it
|
|
54
|
+
# without setting the reimport flag. If someone really wants to turn off
|
|
55
|
+
# reimporting, they should edit the source.
|
|
56
|
+
def self.find_or_create_from(attrs={})
|
|
57
|
+
return new(attrs) unless attrs[:url]
|
|
58
|
+
|
|
59
|
+
source = find_or_create_by_url(attrs[:url])
|
|
60
|
+
source.reimport = true if attrs.delete(:reimport)
|
|
61
|
+
source.attributes = attrs
|
|
62
|
+
source.save if source.changed?
|
|
63
|
+
source
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Create events for this source. Returns the events created. URL must be set
|
|
67
|
+
# for this source for this to work.
|
|
68
|
+
def create_events!(opts={})
|
|
69
|
+
save!
|
|
70
|
+
events = to_events(opts).select(&:valid?)
|
|
71
|
+
events.reject!(&:old?) if opts[:skip_old]
|
|
72
|
+
events.each(&:save!)
|
|
73
|
+
events
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Normalize the URL.
|
|
77
|
+
def url=(value)
|
|
78
|
+
url = URI.parse(value.strip)
|
|
79
|
+
url.scheme = 'http' unless ['http','https','ftp'].include?(url.scheme) || url.scheme.nil?
|
|
80
|
+
write_attribute(:url, url.scheme.nil? ? 'http://'+value.strip : url.to_s)
|
|
81
|
+
rescue URI::InvalidURIError
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns an Array of Event objects that were read from this source.
|
|
86
|
+
#
|
|
87
|
+
# Options:
|
|
88
|
+
# * :url -- URL of data to import. Defaults to record's #url attribute.
|
|
89
|
+
# * :skip_old -- Should old events be skipped? Default is true.
|
|
90
|
+
def to_events(opts={})
|
|
91
|
+
raise ActiveRecord::RecordInvalid, self unless valid?
|
|
92
|
+
|
|
93
|
+
self.imported_at = Time.now
|
|
94
|
+
opts[:url] ||= self.url
|
|
95
|
+
opts[:source] = self
|
|
96
|
+
Source::Parser.to_events(opts)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Return the name of the source, which can be its title or URL.
|
|
100
|
+
def name
|
|
101
|
+
[title, url].detect(&:present?)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Ensure that the URL for this source is valid.
|
|
107
|
+
def assert_url
|
|
108
|
+
URI.parse(url)
|
|
109
|
+
rescue URI::InvalidURIError
|
|
110
|
+
errors.add :url, "has invalid format"
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Calagator
|
|
2
|
+
|
|
3
|
+
class Source < ActiveRecord::Base
|
|
4
|
+
class Importer < Struct.new(:source, :events)
|
|
5
|
+
def initialize params
|
|
6
|
+
self.source = Source.find_or_create_from(params)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def import
|
|
10
|
+
return unless source.valid?
|
|
11
|
+
self.events = source.create_events!
|
|
12
|
+
|
|
13
|
+
self.events.present?
|
|
14
|
+
|
|
15
|
+
rescue Source::Parser::NotFound
|
|
16
|
+
add_error "No events found at remote site. Is the event identifier in the URL correct?"
|
|
17
|
+
rescue Source::Parser::HttpAuthenticationRequiredError
|
|
18
|
+
add_error "Couldn't import events, remote site requires authentication."
|
|
19
|
+
rescue OpenURI::HTTPError
|
|
20
|
+
add_error "Couldn't download events, remote site may be experiencing connectivity problems."
|
|
21
|
+
rescue Errno::EHOSTUNREACH
|
|
22
|
+
add_error "Couldn't connect to remote site."
|
|
23
|
+
rescue SocketError
|
|
24
|
+
add_error "Couldn't find IP address for remote site. Is the URL correct?"
|
|
25
|
+
rescue Exception => e
|
|
26
|
+
add_error "Unknown error: #{e}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def failure_message
|
|
30
|
+
if events.nil?
|
|
31
|
+
"Unable to import: #{source.errors.full_messages.to_sentence}"
|
|
32
|
+
else
|
|
33
|
+
"Unable to find any upcoming events to import from this source"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def add_error message
|
|
40
|
+
source.errors.add :base, message
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "net/https"
|
|
3
|
+
require "open-uri"
|
|
4
|
+
|
|
5
|
+
# == Source::Parser
|
|
6
|
+
#
|
|
7
|
+
# A hierarchy of classes that provide a way to parse different source formats and return hCalendar events.
|
|
8
|
+
# The base class for all format-specific parsers. Do not use instances of this class
|
|
9
|
+
# directly, use a subclass of Parser to do the parsing instead.
|
|
10
|
+
module Calagator
|
|
11
|
+
|
|
12
|
+
class Source::Parser < Struct.new(:opts)
|
|
13
|
+
# Return an Array of unsaved Event instances.
|
|
14
|
+
#
|
|
15
|
+
# Options: (these vary between specific parsers)
|
|
16
|
+
# * :url - URL string to read as parser input.
|
|
17
|
+
# * :content - String to read as parser input.
|
|
18
|
+
def self.to_events(opts)
|
|
19
|
+
# Return events from the first parser that suceeds
|
|
20
|
+
events = matched_parsers(opts[:url]).lazy.collect { |parser|
|
|
21
|
+
parser.new(opts).to_events
|
|
22
|
+
}.detect(&:present?)
|
|
23
|
+
|
|
24
|
+
events || []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.matched_parsers(url)
|
|
28
|
+
# start with the parser that matches the given URL
|
|
29
|
+
parsers.sort_by do |parser|
|
|
30
|
+
match = parser.url_pattern.present? && url.try(:match, parser.url_pattern)
|
|
31
|
+
match ? 0 : 1
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
private_class_method :matched_parsers
|
|
35
|
+
|
|
36
|
+
cattr_accessor(:parsers) { SortedSet.new }
|
|
37
|
+
|
|
38
|
+
def self.inherited(subclass)
|
|
39
|
+
parsers << subclass
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class_attribute :label, :url_pattern
|
|
43
|
+
|
|
44
|
+
# Returns an Array of sorted string labels for the parsers.
|
|
45
|
+
def self.labels
|
|
46
|
+
parsers.map { |p| p.label.to_s }.sort_by(&:downcase)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.read_url(url)
|
|
50
|
+
RestClient.get(url).to_str
|
|
51
|
+
rescue RestClient::Unauthorized
|
|
52
|
+
raise Source::Parser::HttpAuthenticationRequiredError.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_events
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.<=>(other)
|
|
60
|
+
# use site-specific parsers first, then generics alphabetically
|
|
61
|
+
if self.url_pattern && !other.url_pattern
|
|
62
|
+
-1
|
|
63
|
+
elsif !self.url_pattern && other.url_pattern
|
|
64
|
+
1
|
|
65
|
+
else
|
|
66
|
+
self.label <=> other.label
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def event_or_duplicate(event)
|
|
73
|
+
duplicates = event.find_exact_duplicates
|
|
74
|
+
if duplicates.present?
|
|
75
|
+
duplicates.first.progenitor
|
|
76
|
+
else
|
|
77
|
+
event
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def venue_or_duplicate(venue)
|
|
82
|
+
duplicates = venue.find_exact_duplicates
|
|
83
|
+
if duplicates.present?
|
|
84
|
+
duplicates.first.progenitor
|
|
85
|
+
else
|
|
86
|
+
venue_machine_tag_name = venue.tag_list.find { |t|
|
|
87
|
+
# Match 2 in the MACHINE_TAG_PATTERN is the predicate
|
|
88
|
+
ActsAsTaggableOn::Tag::VENUE_PREDICATES.include? t.match(ActsAsTaggableOn::Tag::MACHINE_TAG_PATTERN)[2]
|
|
89
|
+
}
|
|
90
|
+
matched_venue = Venue.tagged_with(venue_machine_tag_name).first
|
|
91
|
+
|
|
92
|
+
if matched_venue.present?
|
|
93
|
+
matched_venue.progenitor
|
|
94
|
+
else
|
|
95
|
+
venue
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_events_api_helper(url, error_key="error", &block)
|
|
101
|
+
# Extract +event_id+ from :url using +url_pattern+.
|
|
102
|
+
event_id = url[self.class.url_pattern, 1]
|
|
103
|
+
return false unless event_id # Give up unless we find the identifier.
|
|
104
|
+
|
|
105
|
+
# Get URL and params for using the API.
|
|
106
|
+
url, params = *block.call(event_id)
|
|
107
|
+
|
|
108
|
+
# Get data from the API.
|
|
109
|
+
data = RestClient.get(url, params: params, accept: "json").to_str
|
|
110
|
+
data = JSON.parse(data)
|
|
111
|
+
|
|
112
|
+
# Stop if API tells us there's an error.
|
|
113
|
+
raise Source::Parser::NotFound, error if error = data[error_key]
|
|
114
|
+
|
|
115
|
+
data['event_id'] = event_id
|
|
116
|
+
data
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
require 'calagator/source/parser/not_found'
|
|
123
|
+
require 'calagator/source/parser/plancast'
|
|
124
|
+
require 'calagator/source/parser/meetup'
|
|
125
|
+
require 'calagator/source/parser/facebook'
|
|
126
|
+
require 'calagator/source/parser/ical'
|
|
127
|
+
require 'calagator/source/parser/hcal'
|
|
128
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Calagator
|
|
2
|
+
|
|
3
|
+
class Source::Parser::Facebook < Source::Parser
|
|
4
|
+
self.label = :Facebook
|
|
5
|
+
|
|
6
|
+
# NOTE: This pattern's goal is to get the Facebook event identifier in the first capture group, so the "(?:foo)" non-capturing group syntax is used to match but not capture those groups -- search the web for "ruby class rexep non-capturing" for details.
|
|
7
|
+
self.url_pattern = %r{(?x) # Ignore regexp whitespace and comments
|
|
8
|
+
^
|
|
9
|
+
(?:https?://)? # Optional http URI prefix
|
|
10
|
+
(?:
|
|
11
|
+
(?:www\.)? # Optional 'www.' host prefix
|
|
12
|
+
(?:
|
|
13
|
+
facebook\.com/events/ # REST-style path
|
|
14
|
+
| # ...or....
|
|
15
|
+
facebook\.com/event\.php\?eid= # GET-style path
|
|
16
|
+
)
|
|
17
|
+
| # ...or...
|
|
18
|
+
graph\.facebook\.com/ # API path
|
|
19
|
+
)
|
|
20
|
+
([^/]+) # Facebook event identifier to capture
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def to_events
|
|
24
|
+
return unless data = to_events_api_helper(opts[:url]) do |event_id|
|
|
25
|
+
"http://graph.facebook.com/#{event_id}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
raise ::Source::Parser::HttpAuthenticationRequiredError if data['parsed_response'] === false
|
|
29
|
+
|
|
30
|
+
event = Event.new({
|
|
31
|
+
source: opts[:source],
|
|
32
|
+
title: data['name'],
|
|
33
|
+
description: data['description'],
|
|
34
|
+
url: opts[:url],
|
|
35
|
+
tag_list: "facebook:event=#{data['id']}",
|
|
36
|
+
venue: to_venue(data),
|
|
37
|
+
|
|
38
|
+
# Facebook is sending floating times, treat them as local
|
|
39
|
+
start_time: Time.zone.parse(data['start_time']),
|
|
40
|
+
end_time: Time.zone.parse(data['end_time']),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
[event_or_duplicate(event)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def to_venue(data)
|
|
49
|
+
fields = (data['venue'] || {})
|
|
50
|
+
return if fields.blank?
|
|
51
|
+
|
|
52
|
+
venue = Venue.new({
|
|
53
|
+
source: opts[:source],
|
|
54
|
+
title: data['location'],
|
|
55
|
+
street_address: fields['street'],
|
|
56
|
+
locality: fields['city'],
|
|
57
|
+
region: fields['state'],
|
|
58
|
+
country: fields['country'],
|
|
59
|
+
latitude: fields['latitude'],
|
|
60
|
+
longitude: fields['longitude'],
|
|
61
|
+
})
|
|
62
|
+
venue.geocode!
|
|
63
|
+
venue_or_duplicate(venue)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|