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.
Files changed (384) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +75 -0
  4. data/Rakefile +12 -0
  5. data/app/assets/images/confused-alligator-sm.png +0 -0
  6. data/app/assets/images/disk.png +0 -0
  7. data/app/assets/images/edit.png +0 -0
  8. data/app/assets/images/external_sites/epdx.png +0 -0
  9. data/app/assets/images/external_sites/external.gif +0 -0
  10. data/app/assets/images/external_sites/facebook.png +0 -0
  11. data/app/assets/images/external_sites/foursquare.png +0 -0
  12. data/app/assets/images/external_sites/gowalla.png +0 -0
  13. data/app/assets/images/external_sites/lanyrd.png +0 -0
  14. data/app/assets/images/external_sites/meetup.png +0 -0
  15. data/app/assets/images/external_sites/plancast.png +0 -0
  16. data/app/assets/images/external_sites/shizzow.png +0 -0
  17. data/app/assets/images/external_sites/upcoming.png +0 -0
  18. data/app/assets/images/external_sites/yelp.png +0 -0
  19. data/app/assets/images/feed.png +0 -0
  20. data/app/assets/images/heart.png +0 -0
  21. data/app/assets/images/icon_ical.gif +0 -0
  22. data/app/assets/images/information.png +0 -0
  23. data/app/assets/images/nav_marker.gif +0 -0
  24. data/app/assets/images/nav_marker.png +0 -0
  25. data/app/assets/images/plus.png +0 -0
  26. data/app/assets/images/redx.png +0 -0
  27. data/app/assets/images/site-icon.png +0 -0
  28. data/app/assets/images/spinner.gif +0 -0
  29. data/app/assets/images/star.png +0 -0
  30. data/app/assets/images/subnav_marker.gif +0 -0
  31. data/app/assets/images/subnav_marker.png +0 -0
  32. data/app/assets/images/tag_blue.png +0 -0
  33. data/app/assets/images/tag_icons/angular.png +0 -0
  34. data/app/assets/images/tag_icons/beer.png +0 -0
  35. data/app/assets/images/tag_icons/bitcoin.png +0 -0
  36. data/app/assets/images/tag_icons/free.png +0 -0
  37. data/app/assets/images/tag_icons/golang.png +0 -0
  38. data/app/assets/images/tag_icons/html.png +0 -0
  39. data/app/assets/images/tag_icons/javascript.png +0 -0
  40. data/app/assets/images/tag_icons/linux.png +0 -0
  41. data/app/assets/images/tag_icons/php.png +0 -0
  42. data/app/assets/images/tag_icons/pizza.png +0 -0
  43. data/app/assets/images/tag_icons/python.png +0 -0
  44. data/app/assets/images/tag_icons/rails.png +0 -0
  45. data/app/assets/images/tag_icons/ruby.png +0 -0
  46. data/app/assets/images/tag_icons/sass.png +0 -0
  47. data/app/assets/images/tag_icons/swift.png +0 -0
  48. data/app/assets/images/transmit_blue.png +0 -0
  49. data/app/assets/images/weekday_background.gif +0 -0
  50. data/app/assets/javascripts/calagator.js +18 -0
  51. data/app/assets/javascripts/calagator/forms.js +60 -0
  52. data/app/assets/stylesheets/calagator.scss +8 -0
  53. data/app/assets/stylesheets/calagator/common.scss +14 -0
  54. data/app/assets/stylesheets/calagator/datepicker.scss +177 -0
  55. data/app/assets/stylesheets/calagator/errors.css +110 -0
  56. data/app/assets/stylesheets/calagator/forms.scss +4 -0
  57. data/app/assets/stylesheets/calagator/reset.css +40 -0
  58. data/app/assets/stylesheets/calagator/theme.css +0 -0
  59. data/app/controllers/calagator/admin_controller.rb +27 -0
  60. data/app/controllers/calagator/application_controller.rb +82 -0
  61. data/app/controllers/calagator/events_controller.rb +171 -0
  62. data/app/controllers/calagator/site_controller.rb +35 -0
  63. data/app/controllers/calagator/sources_controller.rb +94 -0
  64. data/app/controllers/calagator/venues_controller.rb +133 -0
  65. data/app/controllers/calagator/versions_controller.rb +20 -0
  66. data/app/helpers/calagator/application_helper.rb +128 -0
  67. data/app/helpers/calagator/events_helper.rb +184 -0
  68. data/app/helpers/calagator/google_event_export_helper.rb +67 -0
  69. data/app/helpers/calagator/mapping_helper.rb +103 -0
  70. data/app/helpers/calagator/sources_helper.rb +10 -0
  71. data/app/helpers/calagator/tags_helper.rb +42 -0
  72. data/app/helpers/calagator/time_range_helper.rb +166 -0
  73. data/app/models/calagator/event.rb +246 -0
  74. data/app/models/calagator/event/cloner.rb +39 -0
  75. data/app/models/calagator/event/ical_renderer.rb +155 -0
  76. data/app/models/calagator/event/overview.rb +45 -0
  77. data/app/models/calagator/event/saver.rb +58 -0
  78. data/app/models/calagator/event/search.rb +72 -0
  79. data/app/models/calagator/event/search_engine.rb +28 -0
  80. data/app/models/calagator/event/search_engine/apache_sunspot.rb +106 -0
  81. data/app/models/calagator/event/search_engine/sql.rb +107 -0
  82. data/app/models/calagator/source.rb +115 -0
  83. data/app/models/calagator/source/importer.rb +46 -0
  84. data/app/models/calagator/source/parser.rb +128 -0
  85. data/app/models/calagator/source/parser/facebook.rb +67 -0
  86. data/app/models/calagator/source/parser/hcal.rb +108 -0
  87. data/app/models/calagator/source/parser/http_authentication_required_error.rb +6 -0
  88. data/app/models/calagator/source/parser/ical.rb +186 -0
  89. data/app/models/calagator/source/parser/meetup.rb +72 -0
  90. data/app/models/calagator/source/parser/not_found.rb +9 -0
  91. data/app/models/calagator/source/parser/plancast.rb +59 -0
  92. data/app/models/calagator/venue.rb +161 -0
  93. data/app/models/calagator/venue/geocoder.rb +51 -0
  94. data/app/models/calagator/venue/search.rb +63 -0
  95. data/app/models/calagator/venue/search_engine.rb +24 -0
  96. data/app/models/calagator/venue/search_engine/apache_sunspot.rb +85 -0
  97. data/app/models/calagator/venue/search_engine/sql.rb +68 -0
  98. data/app/models/event/search_engine/base.rb +0 -0
  99. data/app/observers/calagator/cache_observer.rb +37 -0
  100. data/app/views/calagator/admin/events.html.erb +28 -0
  101. data/app/views/calagator/admin/index.html.erb +8 -0
  102. data/app/views/calagator/events/_feed_item.html.erb +62 -0
  103. data/app/views/calagator/events/_form.html.erb +63 -0
  104. data/app/views/calagator/events/_gcal_reminder.html.erb +1 -0
  105. data/app/views/calagator/events/_hcal.html.erb +41 -0
  106. data/app/views/calagator/events/_item.html.erb +120 -0
  107. data/app/views/calagator/events/_list.html.erb +30 -0
  108. data/app/views/calagator/events/_list_item.html.erb +37 -0
  109. data/app/views/calagator/events/_search_section.html.erb +15 -0
  110. data/app/views/calagator/events/_subnav.html.erb +13 -0
  111. data/app/views/calagator/events/_table.html.erb +74 -0
  112. data/app/views/calagator/events/duplicates.html.erb +58 -0
  113. data/app/views/calagator/events/edit.html.erb +3 -0
  114. data/app/views/calagator/events/index.atom.builder +32 -0
  115. data/app/views/calagator/events/index.html.erb +51 -0
  116. data/app/views/calagator/events/index.kml.erb +20 -0
  117. data/app/views/calagator/events/new.html.erb +7 -0
  118. data/app/views/calagator/events/search.html.erb +25 -0
  119. data/app/views/calagator/events/show.html.erb +81 -0
  120. data/app/views/calagator/site/_appropriateness_message.html.erb +15 -0
  121. data/app/views/calagator/site/_description.html.erb +3 -0
  122. data/app/views/calagator/site/about.html.erb +7 -0
  123. data/app/views/calagator/site/defunct.html.erb +15 -0
  124. data/app/views/calagator/site/export.html.erb +4 -0
  125. data/app/views/calagator/site/index.html.erb +40 -0
  126. data/app/views/calagator/site/opensearch.xml.builder +8 -0
  127. data/app/views/calagator/sources/_subnav.html.erb +0 -0
  128. data/app/views/calagator/sources/edit.html.erb +12 -0
  129. data/app/views/calagator/sources/import.html.erb +10 -0
  130. data/app/views/calagator/sources/index.html.erb +22 -0
  131. data/app/views/calagator/sources/new.html.erb +32 -0
  132. data/app/views/calagator/sources/show.html.erb +16 -0
  133. data/app/views/calagator/venues/_form.html.erb +57 -0
  134. data/app/views/calagator/venues/_subnav.html.erb +10 -0
  135. data/app/views/calagator/venues/duplicates.html.erb +65 -0
  136. data/app/views/calagator/venues/edit.html.erb +3 -0
  137. data/app/views/calagator/venues/index.html.erb +93 -0
  138. data/app/views/calagator/venues/index.kml.erb +15 -0
  139. data/app/views/calagator/venues/map.html.erb +4 -0
  140. data/app/views/calagator/venues/new.html.erb +5 -0
  141. data/app/views/calagator/venues/show.html.erb +113 -0
  142. data/app/views/calagator/versions/_chooser.html.erb +28 -0
  143. data/app/views/calagator/versions/_edit_with_chooser.html.erb +16 -0
  144. data/app/views/layouts/calagator/application.html.erb +110 -0
  145. data/config/deploy/local.rb +37 -0
  146. data/config/deploy/lucca.rb +33 -0
  147. data/config/initializers/dates.rb +7 -0
  148. data/config/initializers/formtastic.rb +82 -0
  149. data/config/initializers/geokit.rb +74 -0
  150. data/config/initializers/ics_renderer.rb +6 -0
  151. data/config/initializers/load_tag_model_extensions.rb +3 -0
  152. data/config/initializers/mime_types.rb +9 -0
  153. data/config/initializers/observers.rb +1 -0
  154. data/config/initializers/search_engine.rb +4 -0
  155. data/config/initializers/set_default_url_host.rb +5 -0
  156. data/config/initializers/time_get_zone.rb +8 -0
  157. data/config/locales/en.yml +5 -0
  158. data/config/routes.rb +55 -0
  159. data/config/secrets.yml.blag +75 -0
  160. data/config/sunspot.yml +23 -0
  161. data/db/development.sqlite3 +0 -0
  162. data/db/development.sqlite3.bak +0 -0
  163. data/db/development.sqlite3.old +0 -0
  164. data/db/development~20111112@110950.sqlite3 +0 -0
  165. data/db/migrate/001_create_events.rb +17 -0
  166. data/db/migrate/002_create_venues.rb +17 -0
  167. data/db/migrate/003_create_sources.rb +16 -0
  168. data/db/migrate/004_add_detailed_fields_to_venue.rb +19 -0
  169. data/db/migrate/005_add_end_time_to_events.rb +9 -0
  170. data/db/migrate/006_add_source_id_to_events.rb +9 -0
  171. data/db/migrate/008_add_source_id_to_venues.rb +10 -0
  172. data/db/migrate/009_add_duplicate_of_column_to_venues.rb +9 -0
  173. data/db/migrate/010_add_duplicate_of_column_to_events.rb +9 -0
  174. data/db/migrate/011_change_lat_long_type.rb +12 -0
  175. data/db/migrate/012_add_source_reimport.rb +9 -0
  176. data/db/migrate/013_change_end_time_to_duration.rb +11 -0
  177. data/db/migrate/014_remove_format_type_from_source.rb +9 -0
  178. data/db/migrate/015_create_updates.rb +15 -0
  179. data/db/migrate/016_remove_next_update_from_source.rb +9 -0
  180. data/db/migrate/20080705163959_change_duration_to_end_time.rb +11 -0
  181. data/db/migrate/20080705164959_create_tags_and_taggings.rb +28 -0
  182. data/db/migrate/20081011181519_create_versioned_events.rb +25 -0
  183. data/db/migrate/20081011193124_create_versioned_venues.rb +32 -0
  184. data/db/migrate/20081115190515_add_rrule_to_events.rb +15 -0
  185. data/db/migrate/20090912082129_create_versions.rb +18 -0
  186. data/db/migrate/20110219205156_add_closed_flag_to_venues.rb +9 -0
  187. data/db/migrate/20110220001008_add_wifi_flag_to_venues.rb +9 -0
  188. data/db/migrate/20110220011427_add_access_notes_to_venues.rb +9 -0
  189. data/db/migrate/20110220031117_add_events_count_to_venues.rb +8 -0
  190. data/db/migrate/20110604174521_add_venue_details_to_events.rb +9 -0
  191. data/db/migrate/20110717231316_acts_as_taggable_on_migration.rb +50 -0
  192. data/db/migrate/20120709092821_cleanup.rb +14 -0
  193. data/db/migrate/20120831234448_specify_venues_latitude_and_longitude_precision.rb +11 -0
  194. data/db/migrate/20150206085809_remove_updates.rb +13 -0
  195. data/db/migrate/20150207231355_add_locked_status_to_events.rb +5 -0
  196. data/db/production.sqlite3 +0 -0
  197. data/db/schema.rb +102 -0
  198. data/db/seeds.rb +16 -0
  199. data/db/test.sqlite3 +0 -0
  200. data/db/test2.sqlite3 +0 -0
  201. data/lib/calagator.rb +30 -0
  202. data/lib/calagator/blacklist_validator.rb +69 -0
  203. data/lib/calagator/decode_html_entities_hack.rb +33 -0
  204. data/lib/calagator/duplicate_checking.rb +133 -0
  205. data/lib/calagator/duplicate_checking/controller_actions.rb +38 -0
  206. data/lib/calagator/duplicate_checking/duplicate_finder.rb +83 -0
  207. data/lib/calagator/duplicate_checking/duplicate_squasher.rb +74 -0
  208. data/lib/calagator/engine.rb +30 -0
  209. data/lib/calagator/strip_whitespace.rb +19 -0
  210. data/lib/calagator/tag_model_extensions.rb +104 -0
  211. data/lib/calagator/url_prefixer.rb +9 -0
  212. data/lib/calagator/version.rb +3 -0
  213. data/lib/generators/calagator/install_generator.rb +39 -0
  214. data/lib/generators/calagator/templates/config/calagator.rb +26 -0
  215. data/lib/generators/calagator/templates/config/secrets.yml.sample +83 -0
  216. data/lib/secrets_reader.rb +76 -0
  217. data/lib/tasks/spec_db.rake +53 -0
  218. data/lib/tasks/sunspot_reindex_calagator.rake +6 -0
  219. data/lib/tasks/sunspot_solr_restart_enhancements.rake +19 -0
  220. data/lib/tasks/update_counter_caches.rake +13 -0
  221. data/lib/templates/erb/scaffold/_form.html.erb +11 -0
  222. data/lib/theme_reader.rb +17 -0
  223. data/lib/wait_for_solr.rb +25 -0
  224. data/spec/controllers/calagator/application_controller_spec.rb +47 -0
  225. data/spec/controllers/calagator/events_controller_spec.rb +794 -0
  226. data/spec/controllers/calagator/site_controller_spec.rb +59 -0
  227. data/spec/controllers/calagator/sources_controller_spec.rb +439 -0
  228. data/spec/controllers/calagator/venues_controller_spec.rb +319 -0
  229. data/spec/controllers/calagator/versions_controller_spec.rb +82 -0
  230. data/spec/controllers/squash_many_duplicates_examples.rb +49 -0
  231. data/spec/dummy/Gemfile +39 -0
  232. data/spec/dummy/Gemfile.lock +195 -0
  233. data/spec/dummy/README.rdoc +261 -0
  234. data/spec/dummy/Rakefile +7 -0
  235. data/spec/dummy/app/assets/images/rails.png +0 -0
  236. data/spec/dummy/app/assets/javascripts/application.js +16 -0
  237. data/spec/dummy/app/assets/stylesheets/application.css +14 -0
  238. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  239. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  240. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  241. data/spec/dummy/config.ru +4 -0
  242. data/spec/dummy/config/application.rb +62 -0
  243. data/spec/dummy/config/boot.rb +6 -0
  244. data/spec/dummy/config/database.yml +25 -0
  245. data/spec/dummy/config/environment.rb +5 -0
  246. data/spec/dummy/config/environments/development.rb +37 -0
  247. data/spec/dummy/config/environments/production.rb +67 -0
  248. data/spec/dummy/config/environments/test.rb +37 -0
  249. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  250. data/spec/dummy/config/initializers/calagator.rb +26 -0
  251. data/spec/dummy/config/initializers/inflections.rb +15 -0
  252. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  253. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  254. data/spec/dummy/config/initializers/session_store.rb +8 -0
  255. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  256. data/spec/dummy/config/locales/en.yml +5 -0
  257. data/spec/dummy/config/routes.rb +59 -0
  258. data/spec/dummy/config/secrets.yml +83 -0
  259. data/spec/dummy/db/development.sqlite3 +0 -0
  260. data/spec/dummy/db/migrate/20150309023304_create_events.calagator.rb +18 -0
  261. data/spec/dummy/db/migrate/20150309023305_create_venues.calagator.rb +18 -0
  262. data/spec/dummy/db/migrate/20150309023306_create_sources.calagator.rb +17 -0
  263. data/spec/dummy/db/migrate/20150309023307_add_detailed_fields_to_venue.calagator.rb +20 -0
  264. data/spec/dummy/db/migrate/20150309023308_add_end_time_to_events.calagator.rb +10 -0
  265. data/spec/dummy/db/migrate/20150309023309_add_source_id_to_events.calagator.rb +10 -0
  266. data/spec/dummy/db/migrate/20150309023310_add_source_id_to_venues.calagator.rb +11 -0
  267. data/spec/dummy/db/migrate/20150309023311_add_duplicate_of_column_to_venues.calagator.rb +10 -0
  268. data/spec/dummy/db/migrate/20150309023312_add_duplicate_of_column_to_events.calagator.rb +10 -0
  269. data/spec/dummy/db/migrate/20150309023313_change_lat_long_type.calagator.rb +13 -0
  270. data/spec/dummy/db/migrate/20150309023314_add_source_reimport.calagator.rb +10 -0
  271. data/spec/dummy/db/migrate/20150309023315_change_end_time_to_duration.calagator.rb +12 -0
  272. data/spec/dummy/db/migrate/20150309023316_remove_format_type_from_source.calagator.rb +10 -0
  273. data/spec/dummy/db/migrate/20150309023317_create_updates.calagator.rb +16 -0
  274. data/spec/dummy/db/migrate/20150309023318_remove_next_update_from_source.calagator.rb +10 -0
  275. data/spec/dummy/db/migrate/20150309023319_change_duration_to_end_time.calagator.rb +12 -0
  276. data/spec/dummy/db/migrate/20150309023320_create_tags_and_taggings.calagator.rb +29 -0
  277. data/spec/dummy/db/migrate/20150309023321_create_versioned_events.calagator.rb +26 -0
  278. data/spec/dummy/db/migrate/20150309023322_create_versioned_venues.calagator.rb +33 -0
  279. data/spec/dummy/db/migrate/20150309023323_add_rrule_to_events.calagator.rb +16 -0
  280. data/spec/dummy/db/migrate/20150309023324_create_versions.calagator.rb +19 -0
  281. data/spec/dummy/db/migrate/20150309023325_add_closed_flag_to_venues.calagator.rb +10 -0
  282. data/spec/dummy/db/migrate/20150309023326_add_wifi_flag_to_venues.calagator.rb +10 -0
  283. data/spec/dummy/db/migrate/20150309023327_add_access_notes_to_venues.calagator.rb +10 -0
  284. data/spec/dummy/db/migrate/20150309023328_add_events_count_to_venues.calagator.rb +9 -0
  285. data/spec/dummy/db/migrate/20150309023329_add_venue_details_to_events.calagator.rb +10 -0
  286. data/spec/dummy/db/migrate/20150309023330_acts_as_taggable_on_migration.calagator.rb +51 -0
  287. data/spec/dummy/db/migrate/20150309023331_cleanup.calagator.rb +15 -0
  288. data/spec/dummy/db/migrate/20150309023332_specify_venues_latitude_and_longitude_precision.calagator.rb +12 -0
  289. data/spec/dummy/db/migrate/20150309023333_remove_updates.calagator.rb +14 -0
  290. data/spec/dummy/db/migrate/20150309023334_add_locked_status_to_events.calagator.rb +6 -0
  291. data/spec/dummy/db/schema.rb +95 -0
  292. data/spec/dummy/db/seeds.rb +7 -0
  293. data/spec/dummy/db/test.sqlite3 +0 -0
  294. data/spec/dummy/doc/README_FOR_APP +2 -0
  295. data/spec/dummy/log/development.log +273 -0
  296. data/spec/dummy/public/404.html +26 -0
  297. data/spec/dummy/public/422.html +26 -0
  298. data/spec/dummy/public/500.html +25 -0
  299. data/spec/dummy/public/favicon.ico +0 -0
  300. data/spec/dummy/public/robots.txt +5 -0
  301. data/spec/dummy/script/rails +6 -0
  302. data/spec/dummy/test/performance/browsing_test.rb +12 -0
  303. data/spec/dummy/test/test_helper.rb +13 -0
  304. data/spec/factories.rb +93 -0
  305. data/spec/features/add_event_spec.rb +99 -0
  306. data/spec/features/add_venue_spec.rb +34 -0
  307. data/spec/features/admin_auth_spec.rb +22 -0
  308. data/spec/features/admin_lock_event_spec.rb +41 -0
  309. data/spec/features/import_events_from_feed_spec.rb +43 -0
  310. data/spec/features/managing_event_spec.rb +111 -0
  311. data/spec/features/managing_venue_spec.rb +71 -0
  312. data/spec/features/search_event_spec.rb +27 -0
  313. data/spec/helpers/calagator/application_helper_spec.rb +82 -0
  314. data/spec/helpers/calagator/events_helper_spec.rb +172 -0
  315. data/spec/helpers/calagator/google_event_export_helper_spec.rb +70 -0
  316. data/spec/helpers/calagator/sources_helper_spec.rb +12 -0
  317. data/spec/helpers/calagator/tags_helper_spec.rb +85 -0
  318. data/spec/helpers/calagator/time_range_helper_spec.rb +59 -0
  319. data/spec/lib/calagator/blacklist_validator_spec.rb +65 -0
  320. data/spec/lib/calagator/decode_html_entities_hack_spec.rb +54 -0
  321. data/spec/lib/calagator/settings_spec.rb +20 -0
  322. data/spec/lib/calagator/url_prefixer_spec.rb +33 -0
  323. data/spec/lib/secrets_reader_spec.rb +65 -0
  324. data/spec/models/calagator/event/cloner_spec.rb +43 -0
  325. data/spec/models/calagator/event/overview_spec.rb +79 -0
  326. data/spec/models/calagator/event/search_spec.rb +103 -0
  327. data/spec/models/calagator/event_search_spec.rb +149 -0
  328. data/spec/models/calagator/event_spec.rb +859 -0
  329. data/spec/models/calagator/source/parser_facebook_spec.rb +73 -0
  330. data/spec/models/calagator/source/parser_hcal_spec.rb +69 -0
  331. data/spec/models/calagator/source/parser_ical_non_standard_spec.rb +91 -0
  332. data/spec/models/calagator/source/parser_ical_spec.rb +322 -0
  333. data/spec/models/calagator/source/parser_meetup_spec.rb +69 -0
  334. data/spec/models/calagator/source/parser_plancast_spec.rb +53 -0
  335. data/spec/models/calagator/source/parser_spec.rb +238 -0
  336. data/spec/models/calagator/source_spec.rb +135 -0
  337. data/spec/models/calagator/venue/search_spec.rb +92 -0
  338. data/spec/models/calagator/venue_search_spec.rb +124 -0
  339. data/spec/models/calagator/venue_spec.rb +346 -0
  340. data/spec/models/tag_spec.rb +35 -0
  341. data/spec/rails_helper.rb +39 -0
  342. data/spec/spec_helper.rb +140 -0
  343. data/spec/support/disable_geocoding.rb +1 -0
  344. data/spec/support/http_samples.rb +5 -0
  345. data/spec/support/samples/facebook.json +23 -0
  346. data/spec/support/samples/hcal_basic.xml +6 -0
  347. data/spec/support/samples/hcal_dup_event_dup_venue.xml +13 -0
  348. data/spec/support/samples/hcal_event_duplicates_fixture.xml +13 -0
  349. data/spec/support/samples/hcal_event_wo_lat_and_long.xml +14 -0
  350. data/spec/support/samples/hcal_multiple.xml +16 -0
  351. data/spec/support/samples/hcal_same_event_twice_with_different_venues.xml +12 -0
  352. data/spec/support/samples/hcal_single.xml +8 -0
  353. data/spec/support/samples/hcal_two_identical_events.xml +14 -0
  354. data/spec/support/samples/hcal_upcoming_v1.html +412 -0
  355. data/spec/support/samples/hcal_upcoming_v2.html +749 -0
  356. data/spec/support/samples/hcal_upcoming_v3.html +685 -0
  357. data/spec/support/samples/hcal_upcoming_v4.html +761 -0
  358. data/spec/support/samples/ical_apple.ics +22 -0
  359. data/spec/support/samples/ical_apple_v3.ics +37 -0
  360. data/spec/support/samples/ical_basic.ics +15 -0
  361. data/spec/support/samples/ical_basic_with_duration.ics +16 -0
  362. data/spec/support/samples/ical_event_with_squashed_venue.ics +12 -0
  363. data/spec/support/samples/ical_eventful_many.ics +504 -0
  364. data/spec/support/samples/ical_gmt.ics +12 -0
  365. data/spec/support/samples/ical_google.ics +786 -0
  366. data/spec/support/samples/ical_multiple_calendars.ics +111 -0
  367. data/spec/support/samples/ical_upcoming.ics +36 -0
  368. data/spec/support/samples/ical_upcoming_many.ics +682 -0
  369. data/spec/support/samples/ical_upcoming_v2.ics +43 -0
  370. data/spec/support/samples/ical_z.ics +10 -0
  371. data/spec/support/samples/meetup.ics +16 -0
  372. data/spec/support/samples/meetup.json +31 -0
  373. data/spec/support/samples/plancast.ics +59 -0
  374. data/spec/support/samples/plancast.json +51 -0
  375. data/spec/support/samples/plancast_with_missing_venue.json +39 -0
  376. data/spec/support/samples/upcoming_v1.xml +8 -0
  377. data/spec/support/samples/upcoming_v2_with_invalid_utc_dates.xml +8 -0
  378. data/spec/support/time_convenience_methods.rb +8 -0
  379. data/spec/support/time_zones.rb +12 -0
  380. data/spec/support/url_helpers.rb +5 -0
  381. data/spec/support/wait_for_ajax.rb +15 -0
  382. data/spec/support/webmock.rb +8 -0
  383. data/spec/travis_spec.rb +7 -0
  384. 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