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,67 @@
1
+ module Calagator
2
+
3
+ module GoogleEventExportHelper
4
+ # Return a Google Calendar export URL.
5
+
6
+ def google_event_export_link(event)
7
+ GoogleEventExportLink.new(event, self).render
8
+ end
9
+
10
+ class GoogleEventExportLink < Struct.new(:event, :context)
11
+ def render
12
+ truncate(url + query)
13
+ end
14
+
15
+ private
16
+
17
+ def url
18
+ "http://www.google.com/calendar/event?action=TEMPLATE&trp=true&"
19
+ end
20
+
21
+ def query
22
+ fields.collect do |field|
23
+ send(field).to_query(field)
24
+ end.compact.join("&")
25
+ end
26
+
27
+ def fields
28
+ [:text, :dates, :location, :sprop, :details]
29
+ end
30
+
31
+ def text
32
+ event.title
33
+ end
34
+
35
+ def dates
36
+ google_time_format = "%Y%m%dT%H%M%SZ"
37
+ end_time = event.end_time || event.start_time
38
+ "#{event.start_time.utc.strftime(google_time_format)}/#{end_time.utc.strftime(google_time_format)}"
39
+ end
40
+
41
+ def location
42
+ location = event.venue.try(:title)
43
+ if address = event.venue.try(:geocode_address)
44
+ location += ", #{address}" if address.present?
45
+ end
46
+ location
47
+ end
48
+
49
+ def sprop
50
+ if event.url.present?
51
+ "website:#{event.url.sub(/^http.?:\/\//, '')}"
52
+ end
53
+ end
54
+
55
+ def details
56
+ "Imported from: #{context.event_url(event)} \n\n#{event.description}"
57
+ end
58
+
59
+ def truncate string
60
+ omission = "...[truncated]"
61
+ length = 1024 - omission.length
62
+ context.truncate(string, length: length, omission: omission)
63
+ end
64
+ end
65
+ end
66
+
67
+ end
@@ -0,0 +1,103 @@
1
+ module Calagator
2
+
3
+ module MappingHelper
4
+ def map_provider
5
+ (SECRETS.mapping && SECRETS.mapping["provider"]) || 'stamen'
6
+ end
7
+
8
+ def map_tiles
9
+ (SECRETS.mapping && SECRETS.mapping["tiles"]) || 'terrain'
10
+ end
11
+
12
+ def leaflet_js
13
+ Rails.env.production? ? ["https://d591zijq8zntj.cloudfront.net/leaflet-0.6.4/leaflet.js"] : ["leaflet"]
14
+ end
15
+
16
+ def map_provider_dependencies
17
+ {
18
+ "stamen" => ["http://maps.stamen.com/js/tile.stamen.js?v1.2.3"],
19
+ "mapbox" => ["https://api.tiles.mapbox.com/mapbox.js/v1.3.1/mapbox.standalone.js"],
20
+ "esri" => ["http://cdn-geoweb.s3.amazonaws.com/esri-leaflet/0.0.1-beta.5/esri-leaflet.js"],
21
+ "google" => [
22
+ "https://maps.googleapis.com/maps/api/js?key=#{SECRETS.mapping && SECRETS.mapping["google_maps_api_key"]}&sensor=false",
23
+ "leaflet_google_layer",
24
+ ],
25
+ }[map_provider]
26
+ end
27
+
28
+ def mapping_js_includes
29
+ leaflet_js + map_provider_dependencies
30
+ end
31
+
32
+ def map(locatable_items, options = {})
33
+ options.symbolize_keys!
34
+ locatable_items = Array(locatable_items).select{|i| i.location.present? }
35
+
36
+ if locatable_items.present?
37
+ div_id = options[:id] || 'map'
38
+ map_div = content_tag(:div, "", :id => div_id)
39
+
40
+ markers = map_markers(locatable_items)
41
+ zoom = options[:zoom] || 14
42
+ center = (options[:center] || locatable_items.first.location).join(", ")
43
+ should_fit_bounds = locatable_items.count > 1 && options[:center].blank?
44
+
45
+ script = <<-JS
46
+ var layer = new #{layer_constructor}("#{map_tiles}");
47
+ var map = new L.Map("#{div_id}", {
48
+ center: new L.LatLng(#{center}),
49
+ zoom: #{zoom},
50
+ attributionControl: false
51
+ });
52
+ L.control.attribution ({
53
+ position: 'bottomright',
54
+ prefix: false
55
+ }).addTo(map);
56
+
57
+ map.addLayer(layer);
58
+
59
+ var venueIcon = L.AwesomeMarkers.icon({
60
+ icon: 'star',
61
+ color: '#{SECRETS.mapping['marker_color']}'
62
+ })
63
+
64
+ var markers = [#{markers.join(", ")}];
65
+ var markerGroup = L.featureGroup(markers);
66
+ markerGroup.addTo(map);
67
+ JS
68
+ script << "map.fitBounds(markerGroup.getBounds());" if should_fit_bounds
69
+
70
+ map_div + javascript_tag(script)
71
+ end
72
+ end
73
+
74
+ alias_method :google_map, :map
75
+
76
+ def layer_constructor
77
+ constructor_map = {
78
+ "stamen" => "L.StamenTileLayer",
79
+ "mapbox" => "L.mapbox.tileLayer",
80
+ "esri" => "L.esri.basemapLayer",
81
+ "google" => "L.Google",
82
+ "leaflet" => "L.tileLayer",
83
+ }
84
+ constructor_map[map_provider]
85
+ end
86
+
87
+ def map_markers(locatable_items)
88
+ Array(locatable_items).map { |locatable_item|
89
+ location = locatable_item.location
90
+
91
+ if location
92
+ latitude = location[0]
93
+ longitude = location[1]
94
+ title = locatable_item.title
95
+ popup = link_to(locatable_item.title, locatable_item)
96
+
97
+ "L.marker([#{latitude}, #{longitude}], {title: '#{j title}', icon: venueIcon}).bindPopup('#{j popup}')"
98
+ end
99
+ }.compact
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,10 @@
1
+ module Calagator
2
+
3
+ module SourcesHelper
4
+ # Return HTML with a link to the the external source URL.
5
+ def source_url_link(source)
6
+ return link_to source.url, source.url, { :rel => "nofollow", :target => "_BLANK" }
7
+ end
8
+ end
9
+
10
+ end
@@ -0,0 +1,42 @@
1
+ module Calagator
2
+
3
+ module TagsHelper
4
+ def tag_links_for(model)
5
+ model.tags.sort_by(&:name).map{|tag| tag_link(model.class.model_name.human.downcase, tag)}.join(', ').html_safe
6
+ end
7
+
8
+ def tag_link(type, tag, link_class=nil)
9
+ internal_url = "/#{type.pluralize}/tag/#{tag.name}"
10
+
11
+ link_classes = [link_class, "p-category"]
12
+ link_classes << "external #{tag.machine_tag[:namespace]} #{tag.machine_tag[:predicate]}" if tag.machine_tag[:url]
13
+
14
+ link_text = [tag_icon(tag.name), escape_once(tag.name)].compact.join(' ').html_safe
15
+
16
+ link_to link_text, (tag.machine_tag[:url] || internal_url), :class => link_classes.compact.join(' ')
17
+ end
18
+ private :tag_link
19
+
20
+ def icon_exists_for?(tag_name)
21
+ !!Rails.application.assets["tag_icons/#{tag_name}.png"]
22
+ end
23
+
24
+ def tag_icon(tag_name)
25
+ if icon_exists_for?(tag_name)
26
+ image_tag(asset_path("tag_icons/#{tag_name}.png"), title: tag_name)
27
+ end
28
+ end
29
+
30
+ def get_tag_icon_links(event)
31
+ event.tag_list.map do |tag_name|
32
+ icon = tag_icon(tag_name)
33
+ link_to(icon, tag_events_path(tag_name)) if icon
34
+ end
35
+ end
36
+
37
+ def display_tag_icons(event)
38
+ get_tag_icon_links(event).join(' ').html_safe
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,166 @@
1
+ module Calagator
2
+
3
+ module TimeRangeHelper
4
+ extend self
5
+ # Initialize with a single DateTime, a pair of DateTimes,
6
+ # or an object that responds_to start_time and end_time, and two options
7
+ # By default (unless :format => :text) include <abbr> tags for hCalendar,
8
+ # if a :context date is provided, omit unnecessary date parts.
9
+ def normalize_time(start_time, end_time=nil, format: :hcal, context: nil)
10
+ if end_time.nil? and start_time.respond_to?(:start_time)
11
+ end_time = start_time.end_time
12
+ start_time = start_time.start_time
13
+ end
14
+ TimeRange.new(start_time, end_time, format, context).to_s.html_safe
15
+ end
16
+
17
+ class TimeRange < Struct.new(:start_time, :end_time, :format, :context_date)
18
+ # A representation of a time or range of time that can format itself
19
+ # in a meaningful way. Examples:
20
+ # "Thursday, April 3, 2008"
21
+ # "Thursday, April 3, 2008 at 4pm"
22
+ # "Thursday, April 3, 2008 from 4:30-6pm"
23
+ # (context: in the list for today) "11:30am-2pm"
24
+ # "Thursday-Friday, April 3-5, 2008"
25
+ # (context: during 2008) "Thursday April 5, 2009 at 3:30pm through Friday, April 5 at 8:45pm, 2009"
26
+ # (same, context: during 2009) "Thursday April 5 at 3:30pm through Friday, April 5 at 8:45pm"
27
+ def to_s
28
+ [start_text, conjunction, end_text].compact.join
29
+ end
30
+
31
+ private
32
+
33
+ def start_text
34
+ part = TimePart.new(start_time, context_date, from_prefix: same_day?, no_meridian: same_meridian?)
35
+ part.to_s(format, "start")
36
+ end
37
+
38
+ def conjunction
39
+ return unless range?
40
+ if same_day?
41
+ format == :text ? "-" : "&ndash;"
42
+ else
43
+ " through "
44
+ end
45
+ end
46
+
47
+ def end_text
48
+ return unless range?
49
+ part = TimePart.new(end_time, context_date, time_only: same_day?)
50
+ part.to_s(format, "end")
51
+ end
52
+
53
+ def range?
54
+ end_time.present? && start_time != end_time
55
+ end
56
+
57
+ def same_day?
58
+ range? && start_time.to_date == end_time.to_date
59
+ end
60
+
61
+ def same_meridian?
62
+ same_day? && start_time.strftime("%p") == end_time.strftime("%p")
63
+ end
64
+ end
65
+
66
+ class TimePart
67
+ def initialize(time, context, time_only: false, no_meridian: false, from_prefix: false)
68
+ @time = time
69
+ @context = context
70
+ @parts = get_parts
71
+ remove_parts_implied_by_context
72
+ remove_day_parts if time_only
73
+ remove_suffix if no_meridian
74
+ set_at_to_from if from_prefix
75
+ end
76
+
77
+ attr_reader :time, :context, :parts
78
+
79
+ def to_s(format, which)
80
+ TimePartRenderer.new(self, format, which).to_s
81
+ end
82
+
83
+ private
84
+
85
+ def get_parts
86
+ parts = {
87
+ wday: time.strftime("%A"),
88
+ month: time.strftime("%B"),
89
+ day: time.day,
90
+ year: time.year,
91
+ at: "at",
92
+ hour: time.strftime("%l").strip,
93
+ min: time.strftime(":%M"),
94
+ suffix: time.strftime("%P"),
95
+ }
96
+ if time.min == 0
97
+ parts.delete(:min)
98
+ return parts.merge(hour: "midnight", suffix: nil) if time.hour == 0
99
+ return parts.merge(hour: "noon", suffix: nil) if time.hour == 12
100
+ end
101
+ parts
102
+ end
103
+
104
+ def remove_parts_implied_by_context
105
+ return unless time && context
106
+ parts.delete(:year) if context.year == time.year
107
+ [:wday, :month, :day, :at, :from].each do |key|
108
+ parts.delete(key)
109
+ end if time.to_date == context
110
+ end
111
+
112
+ def remove_day_parts
113
+ parts.keep_if { |key| [:hour, :min, :suffix].include?(key) }
114
+ end
115
+
116
+ def remove_suffix
117
+ parts.delete(:suffix)
118
+ end
119
+
120
+ def set_at_to_from
121
+ parts[:at] = "from" if parts.has_key?(:at)
122
+ end
123
+ end
124
+
125
+ class TimePartRenderer < Struct.new(:part, :format, :which)
126
+ PREFIXES = {
127
+ :hour => " ",
128
+ [nil, :hour] => "",
129
+ :year => ", ",
130
+ :end_hour => " ",
131
+ :end_year => ", ",
132
+ :at => " ",
133
+ }
134
+ SUFFIXES = {
135
+ :month => " ",
136
+ :wday => ", ",
137
+ }
138
+
139
+ def to_s
140
+ if format == :hcal
141
+ wrap_in_hcal(text, which)
142
+ else
143
+ text
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def text
150
+ part.parts.reduce("") do |string, (key, value)|
151
+ prefix = (PREFIXES[[@last_key, key]] || PREFIXES[key])
152
+ suffix = SUFFIXES[key]
153
+ @last_key = key
154
+ "#{string}#{prefix}#{value}#{suffix}"
155
+ end
156
+ end
157
+
158
+ def wrap_in_hcal(string, which)
159
+ css_class = "dt#{which} dt-#{which}"
160
+ formatted_time = part.time.strftime('%Y-%m-%dT%H:%M:%S')
161
+ %(<time class="#{css_class}" title="#{formatted_time}" datetime="#{formatted_time}">#{string}</time>)
162
+ end
163
+ end
164
+ end
165
+
166
+ end
@@ -0,0 +1,246 @@
1
+ # == Schema Information
2
+ # Schema version: 20110604174521
3
+ #
4
+ # Table name: events
5
+ #
6
+ # id :integer not null, primary key
7
+ # title :string(255)
8
+ # description :text
9
+ # start_time :datetime
10
+ # url :string(255)
11
+ # created_at :datetime
12
+ # updated_at :datetime
13
+ # venue_id :integer
14
+ # source_id :integer
15
+ # duplicate_of_id :integer
16
+ # end_time :datetime
17
+ # version :integer
18
+ # rrule :string(255)
19
+ # venue_details :text
20
+ #
21
+
22
+ require "calagator/blacklist_validator"
23
+ require "calagator/duplicate_checking"
24
+ require "calagator/decode_html_entities_hack"
25
+ require "calagator/strip_whitespace"
26
+ require "calagator/url_prefixer"
27
+ require "paper_trail"
28
+ require "loofah-activerecord"
29
+ require "loofah/activerecord/xss_foliate"
30
+
31
+ # == Event
32
+ #
33
+ # A model representing a calendar event.
34
+
35
+ module Calagator
36
+ class Event < ActiveRecord::Base
37
+ self.table_name = "events"
38
+
39
+ attr_protected
40
+
41
+ has_paper_trail
42
+ acts_as_taggable
43
+
44
+ xss_foliate :strip => [:title], :sanitize => [:description, :venue_details]
45
+ include DecodeHtmlEntitiesHack
46
+
47
+ # Associations
48
+ belongs_to :venue, :counter_cache => true
49
+ belongs_to :source
50
+
51
+ # Validations
52
+ validates_presence_of :title, :start_time
53
+ validate :end_time_later_than_start_time
54
+ validates_format_of :url,
55
+ :with => /\Ahttps?:\/\/(\w+:?\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?\Z/,
56
+ :allow_blank => true,
57
+ :allow_nil => true
58
+
59
+ validates :title, :description, :url, blacklist: true
60
+
61
+ before_destroy :verify_lock_status
62
+
63
+ # Duplicates
64
+ include DuplicateChecking
65
+ duplicate_checking_ignores_attributes :source_id, :version, :venue_id
66
+ duplicate_squashing_ignores_associations :tags, :base_tags, :taggings
67
+
68
+ # Named scopes
69
+ scope :after_date, lambda { |date|
70
+ where(["start_time >= ?", date]).order(:start_time)
71
+ }
72
+ scope :on_or_after_date, lambda { |date|
73
+ time = date.beginning_of_day
74
+ where("(start_time >= :time) OR (end_time IS NOT NULL AND end_time > :time)",
75
+ :time => time).order(:start_time)
76
+ }
77
+ scope :before_date, lambda { |date|
78
+ time = date.beginning_of_day
79
+ where("start_time < :time", :time => time).order(:start_time)
80
+ }
81
+ scope :future, lambda { on_or_after_date(Time.zone.today) }
82
+ scope :past, lambda { before_date(Time.zone.today) }
83
+ scope :within_dates, lambda { |start_date, end_date|
84
+ if start_date == end_date
85
+ end_date = end_date + 1.day
86
+ end
87
+ on_or_after_date(start_date).before_date(end_date)
88
+ }
89
+
90
+ scope :future_with_venue, -> {
91
+ future.order("start_time ASC").non_duplicates.includes(:venue)
92
+ }
93
+ scope :past_with_venue, -> {
94
+ past.order("start_time DESC").non_duplicates.includes(:venue)
95
+ }
96
+
97
+ # Expand the simple sort order names from the URL into more intelligent SQL order strings
98
+ scope :ordered_by_ui_field, lambda{|ui_field|
99
+ case ui_field
100
+ when 'name'
101
+ order('lower(events.title), start_time')
102
+ when 'venue'
103
+ includes(:venue).order('lower(venues.title), start_time')
104
+ else # when 'date', nil
105
+ order('start_time')
106
+ end
107
+ }
108
+
109
+ #---[ Overrides ]-------------------------------------------------------
110
+
111
+ # Return the title but strip out any whitespace.
112
+ def title
113
+ # TODO Generalize this code so we can use it on other attributes in the different model classes. The solution should use an #alias_method_chain to make sure it's not breaking any explicit overrides for an attribute.
114
+ read_attribute(:title).to_s.strip
115
+ end
116
+
117
+ # Return description without those pesky carriage-returns.
118
+ def description
119
+ # TODO Generalize this code so we can reuse it on other attributes.
120
+ read_attribute(:description).to_s.gsub("\r\n", "\n").gsub("\r", "\n")
121
+ end
122
+
123
+ def url=(value)
124
+ super UrlPrefixer.prefix(value)
125
+ end
126
+
127
+ # Set the start_time to the given +value+, which could be a Time, Date,
128
+ # DateTime, String, Array of Strings, or nil.
129
+ def start_time=(value)
130
+ super time_for(value)
131
+ rescue ArgumentError
132
+ errors.add :start_time, "is invalid"
133
+ super nil
134
+ end
135
+
136
+ # Set the end_time to the given +value+, which could be a Time, Date,
137
+ # DateTime, String, Array of Strings, or nil.
138
+ def end_time=(value)
139
+ super time_for(value)
140
+ rescue ArgumentError
141
+ errors.add :end_time, "is invalid"
142
+ super nil
143
+ end
144
+
145
+ def time_for(value)
146
+ value = value.join(' ') if value.kind_of?(Array)
147
+ value = Time.zone.parse(value) if value.kind_of?(String) # this will throw ArgumentError if invalid
148
+ value
149
+ end
150
+ private :time_for
151
+
152
+ def lock_editing!
153
+ update_attribute(:locked, true)
154
+ end
155
+
156
+ def unlock_editing!
157
+ update_attribute(:locked, false)
158
+ end
159
+
160
+ #---[ Queries ]---------------------------------------------------------
161
+
162
+ # Return Hash of Events grouped by the +type+.
163
+ def self.find_duplicates_by_type(type='na')
164
+ case type.to_s.strip
165
+ when 'na', ''
166
+ { [] => future }
167
+ else
168
+ kind = %w[all any].include?(type) ? type.to_sym : type.split(',').map(&:to_sym)
169
+ find_duplicates_by(kind,
170
+ :grouped => true,
171
+ :where => "a.start_time >= #{connection.quote(Time.now - 1.day)}")
172
+ end
173
+ end
174
+
175
+ #---[ Searching ]-------------------------------------------------------
176
+
177
+ # NOTE: The `Event.search` method is implemented elsewhere! For example, it's
178
+ # added by SearchEngine::ActsAsSolr if you're using that search engine.
179
+
180
+ def self.search_tag(tag, opts={})
181
+ includes(:venue).tagged_with(tag).ordered_by_ui_field(opts[:order])
182
+ end
183
+
184
+ def self.search(query, opts={})
185
+ SearchEngine.search(query, opts)
186
+ end
187
+
188
+ #---[ Transformations ]-------------------------------------------------
189
+
190
+ def location
191
+ venue && venue.location
192
+ end
193
+
194
+ def venue_title
195
+ venue && venue.title
196
+ end
197
+
198
+ #---[ Date related ]----------------------------------------------------
199
+
200
+ # Returns an array of the dates spanned by the event.
201
+ def dates
202
+ raise ArgumentError, "can't get dates for an event with no start time" unless start_time
203
+ if end_time
204
+ (start_time.to_date..end_time.to_date).to_a
205
+ else
206
+ [start_time.to_date]
207
+ end
208
+ end
209
+
210
+ # Is this event current?
211
+ def current?
212
+ (end_time || start_time) >= Date.today.to_time
213
+ end
214
+
215
+ # Is this event old?
216
+ def old?
217
+ (end_time || start_time + 1.hour) <= Time.zone.now.beginning_of_day
218
+ end
219
+
220
+ # Did this event start before today but ends today or later?
221
+ def ongoing?
222
+ start_time < Date.today.to_time && end_time && end_time >= Date.today.to_time
223
+ end
224
+
225
+ def duration
226
+ if end_time && start_time
227
+ (end_time - start_time)
228
+ else
229
+ 0
230
+ end
231
+ end
232
+
233
+ protected
234
+
235
+ def end_time_later_than_start_time
236
+ if start_time && end_time && end_time < start_time
237
+ errors.add(:end_time, "cannot be before start")
238
+ end
239
+ end
240
+
241
+ def verify_lock_status
242
+ return !locked
243
+ end
244
+ end
245
+
246
+ end