landable 1.7.0

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 (310) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +15 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +56 -0
  7. data/Rakefile +22 -0
  8. data/app/controllers/concerns/landable/variables_concern.rb +38 -0
  9. data/app/controllers/landable/api/access_tokens_controller.rb +48 -0
  10. data/app/controllers/landable/api/assets_controller.rb +55 -0
  11. data/app/controllers/landable/api/categories_controller.rb +13 -0
  12. data/app/controllers/landable/api/directories_controller.rb +19 -0
  13. data/app/controllers/landable/api/page_revisions_controller.rb +38 -0
  14. data/app/controllers/landable/api/pages_controller.rb +96 -0
  15. data/app/controllers/landable/api/templates_controller.rb +33 -0
  16. data/app/controllers/landable/api/themes_controller.rb +57 -0
  17. data/app/controllers/landable/api_controller.rb +75 -0
  18. data/app/controllers/landable/application_controller.rb +4 -0
  19. data/app/controllers/landable/public/pages_controller.rb +25 -0
  20. data/app/controllers/landable/public/preview/page_revisions_controller.rb +16 -0
  21. data/app/controllers/landable/public/preview/pages_controller.rb +16 -0
  22. data/app/controllers/landable/public/sitemap_controller.rb +18 -0
  23. data/app/decorators/landable/null_page_decorator.rb +26 -0
  24. data/app/decorators/landable/page_decorator.rb +40 -0
  25. data/app/helpers/landable/application_helper.rb +13 -0
  26. data/app/helpers/landable/pages_helper.rb +20 -0
  27. data/app/models/concerns/landable/has_assets.rb +74 -0
  28. data/app/models/concerns/landable/table_name.rb +12 -0
  29. data/app/models/concerns/landable/traffic/table_name.rb +14 -0
  30. data/app/models/landable/access_token.rb +20 -0
  31. data/app/models/landable/asset.rb +63 -0
  32. data/app/models/landable/asset_search_engine.rb +20 -0
  33. data/app/models/landable/author.rb +12 -0
  34. data/app/models/landable/category.rb +27 -0
  35. data/app/models/landable/directory.rb +23 -0
  36. data/app/models/landable/page.rb +259 -0
  37. data/app/models/landable/page_revision.rb +74 -0
  38. data/app/models/landable/page_search_engine.rb +20 -0
  39. data/app/models/landable/search_engine.rb +39 -0
  40. data/app/models/landable/template.rb +24 -0
  41. data/app/models/landable/theme.rb +20 -0
  42. data/app/models/landable/traffic/access.rb +11 -0
  43. data/app/models/landable/traffic/ad_group.rb +11 -0
  44. data/app/models/landable/traffic/ad_type.rb +11 -0
  45. data/app/models/landable/traffic/attribution.rb +38 -0
  46. data/app/models/landable/traffic/bid_match_type.rb +11 -0
  47. data/app/models/landable/traffic/browser.rb +11 -0
  48. data/app/models/landable/traffic/campaign.rb +11 -0
  49. data/app/models/landable/traffic/city.rb +11 -0
  50. data/app/models/landable/traffic/content.rb +11 -0
  51. data/app/models/landable/traffic/cookie.rb +16 -0
  52. data/app/models/landable/traffic/country.rb +11 -0
  53. data/app/models/landable/traffic/creative.rb +11 -0
  54. data/app/models/landable/traffic/device.rb +11 -0
  55. data/app/models/landable/traffic/device_type.rb +11 -0
  56. data/app/models/landable/traffic/domain.rb +12 -0
  57. data/app/models/landable/traffic/event.rb +12 -0
  58. data/app/models/landable/traffic/event_type.rb +11 -0
  59. data/app/models/landable/traffic/experiment.rb +11 -0
  60. data/app/models/landable/traffic/http_method.rb +11 -0
  61. data/app/models/landable/traffic/ip_address.rb +11 -0
  62. data/app/models/landable/traffic/ip_lookup.rb +12 -0
  63. data/app/models/landable/traffic/keyword.rb +11 -0
  64. data/app/models/landable/traffic/location.rb +11 -0
  65. data/app/models/landable/traffic/match_type.rb +11 -0
  66. data/app/models/landable/traffic/medium.rb +11 -0
  67. data/app/models/landable/traffic/mime_type.rb +11 -0
  68. data/app/models/landable/traffic/network.rb +11 -0
  69. data/app/models/landable/traffic/owner.rb +10 -0
  70. data/app/models/landable/traffic/ownership.rb +10 -0
  71. data/app/models/landable/traffic/page_view.rb +24 -0
  72. data/app/models/landable/traffic/path.rb +13 -0
  73. data/app/models/landable/traffic/placement.rb +11 -0
  74. data/app/models/landable/traffic/platform.rb +11 -0
  75. data/app/models/landable/traffic/position.rb +11 -0
  76. data/app/models/landable/traffic/query_string.rb +12 -0
  77. data/app/models/landable/traffic/referer.rb +11 -0
  78. data/app/models/landable/traffic/region.rb +11 -0
  79. data/app/models/landable/traffic/search_term.rb +11 -0
  80. data/app/models/landable/traffic/source.rb +11 -0
  81. data/app/models/landable/traffic/target.rb +11 -0
  82. data/app/models/landable/traffic/user_agent.rb +28 -0
  83. data/app/models/landable/traffic/user_agent_type.rb +11 -0
  84. data/app/models/landable/traffic/visit.rb +15 -0
  85. data/app/models/landable/traffic/visitor.rb +13 -0
  86. data/app/responders/landable/api_responder.rb +76 -0
  87. data/app/responders/landable/page_render_responder.rb +15 -0
  88. data/app/serializers/landable/access_token_serializer.rb +6 -0
  89. data/app/serializers/landable/asset_serializer.rb +14 -0
  90. data/app/serializers/landable/author_serializer.rb +5 -0
  91. data/app/serializers/landable/category_serializer.rb +5 -0
  92. data/app/serializers/landable/directory_serializer.rb +8 -0
  93. data/app/serializers/landable/page_revision_serializer.rb +15 -0
  94. data/app/serializers/landable/page_serializer.rb +31 -0
  95. data/app/serializers/landable/template_serializer.rb +5 -0
  96. data/app/serializers/landable/theme_serializer.rb +7 -0
  97. data/app/services/landable/authentication_service.rb +44 -0
  98. data/app/services/landable/registration_service.rb +13 -0
  99. data/app/services/landable/render_service.rb +86 -0
  100. data/app/services/landable/tidy_service.rb +155 -0
  101. data/app/uploaders/landable/asset_uploader.rb +9 -0
  102. data/app/validators/path_validator.rb +12 -0
  103. data/app/validators/url_validator.rb +13 -0
  104. data/app/views/templates/preview.liquid +122 -0
  105. data/bin/rails +8 -0
  106. data/bin/redb +7 -0
  107. data/config.ru +7 -0
  108. data/config/cucumber.yml +5 -0
  109. data/config/routes.rb +62 -0
  110. data/db/migrate/20130510221424_create_landable_schema.rb +338 -0
  111. data/db/migrate/20130909182713_landable_pages__add_updated_by.rb +11 -0
  112. data/db/migrate/20130909182715_landable_page_revisions__break_out_snapshot.rb +72 -0
  113. data/db/migrate/20130909191153_landable_pages__add_lock_version.rb +5 -0
  114. data/db/migrate/20131002220041_file_based_themes.rb +12 -0
  115. data/db/migrate/20131008164204_create_head_tag_on_page.rb +19 -0
  116. data/db/migrate/20131008193544_drop_status_codes_model.rb +44 -0
  117. data/db/migrate/20131028145652_add_traffic_schema.rb +276 -0
  118. data/db/migrate/20131101213623_add_dnt_column_to_visits.rb +7 -0
  119. data/db/migrate/20131104224120_add_meta_on_events.rb +7 -0
  120. data/db/migrate/20131106185946_add_index_on_page_revisions_path.rb +6 -0
  121. data/db/migrate/20131106193021_page_revisisons__path_status_code_index.rb +9 -0
  122. data/db/migrate/20131108212501_traffic_owner_ids_are_serials.rb +20 -0
  123. data/db/migrate/20131121150902_add_attribution_id_to_unique_index.rb +10 -0
  124. data/db/migrate/20131216214027_drop_browser_screenshot_tables.rb +6 -0
  125. data/db/migrate/20140128170659_file_backed_templates.rb +8 -0
  126. data/db/migrate/20140205193757_fix_status_codes.rb +24 -0
  127. data/db/migrate/20140206211322_add_response_time_to_traffic_page_views.rb +7 -0
  128. data/db/migrate/20140220170324_add_slug_to_categories.rb +14 -0
  129. data/db/migrate/20140220174630_add_abstract_and_hero_asset_to_pages_and_page_revisions.rb +8 -0
  130. data/db/migrate/20140224205516_rename_traffic_schema.rb +40 -0
  131. data/db/pgtap/pgtap.sql +9034 -0
  132. data/db/test/landable.access_tokens.sql +13 -0
  133. data/db/test/landable.assets.sql +16 -0
  134. data/db/test/landable.authors.sql +22 -0
  135. data/db/test/landable.categories.sql +9 -0
  136. data/db/test/landable.page_revisions.sql +41 -0
  137. data/db/test/landable.pages.sql +19 -0
  138. data/db/test/landable.templates.sql +15 -0
  139. data/db/test/landable.themes.sql +25 -0
  140. data/doc/schema/access_token.json +22 -0
  141. data/doc/schema/asset.json +65 -0
  142. data/doc/schema/author.json +30 -0
  143. data/doc/schema/directory.json +24 -0
  144. data/doc/schema/page.json +95 -0
  145. data/doc/schema/page_revision.json +70 -0
  146. data/doc/schema/theme.json +37 -0
  147. data/doc/schema/uuid.json +6 -0
  148. data/features/api/access_tokens.feature +84 -0
  149. data/features/api/assets.feature +46 -0
  150. data/features/api/cors.feature +25 -0
  151. data/features/api/pages.feature +42 -0
  152. data/features/api/preview.feature +16 -0
  153. data/features/api/templates.feature +33 -0
  154. data/features/api/themes.feature +33 -0
  155. data/features/liquid/body.feature +35 -0
  156. data/features/liquid/drops/categories.feature +54 -0
  157. data/features/liquid/tags.feature +168 -0
  158. data/features/public/content_types.feature +17 -0
  159. data/features/public/publishing.feature +45 -0
  160. data/features/public/status_codes.feature +25 -0
  161. data/features/public/views.feature +17 -0
  162. data/features/step_definitions/asset_steps.rb +60 -0
  163. data/features/step_definitions/core_api_steps.rb +139 -0
  164. data/features/step_definitions/debug_steps.rb +3 -0
  165. data/features/step_definitions/factory_steps.rb +124 -0
  166. data/features/step_definitions/html_steps.rb +9 -0
  167. data/features/step_definitions/liquid_steps.rb +79 -0
  168. data/features/step_definitions/revision_steps.rb +5 -0
  169. data/features/step_definitions/theme_steps.rb +43 -0
  170. data/features/support/env.rb +66 -0
  171. data/features/support/usefulness.rb +13 -0
  172. data/landable.gemspec +54 -0
  173. data/lib/generators/landable/collection/collection_generator.rb +0 -0
  174. data/lib/generators/landable/collection/templates/stylesheets/landable/%file_name%.less +0 -0
  175. data/lib/generators/landable/collection/templates/stylesheets/landable/%file_name%/mixins.less +0 -0
  176. data/lib/generators/landable/collection/templates/stylesheets/landable/%file_name%/variables.less +0 -0
  177. data/lib/generators/landable/component/component_generator.rb +0 -0
  178. data/lib/generators/landable/component/templates/javascripts/landable/%file_name%.less +0 -0
  179. data/lib/generators/landable/component/templates/stylesheets/landable/%file_name%.less +0 -0
  180. data/lib/generators/landable/install_generator.rb +19 -0
  181. data/lib/generators/landable/landable_generator.rb +22 -0
  182. data/lib/generators/templates/landable.rb +34 -0
  183. data/lib/landable.rb +30 -0
  184. data/lib/landable/configuration.rb +115 -0
  185. data/lib/landable/core_ext/ipaddr.rb +18 -0
  186. data/lib/landable/engine.rb +69 -0
  187. data/lib/landable/error.rb +16 -0
  188. data/lib/landable/inflections.rb +4 -0
  189. data/lib/landable/layout.rb +60 -0
  190. data/lib/landable/liquid.rb +27 -0
  191. data/lib/landable/liquid/asset_tags.rb +76 -0
  192. data/lib/landable/liquid/drops.rb +46 -0
  193. data/lib/landable/liquid/filters.rb +11 -0
  194. data/lib/landable/liquid/tags.rb +91 -0
  195. data/lib/landable/migration.rb +40 -0
  196. data/lib/landable/mime_types.rb +15 -0
  197. data/lib/landable/partial.rb +46 -0
  198. data/lib/landable/seeds.rb +36 -0
  199. data/lib/landable/traffic.rb +34 -0
  200. data/lib/landable/traffic/crawl_tracker.rb +9 -0
  201. data/lib/landable/traffic/noop_tracker.rb +8 -0
  202. data/lib/landable/traffic/ping_tracker.rb +9 -0
  203. data/lib/landable/traffic/scan_tracker.rb +9 -0
  204. data/lib/landable/traffic/scrape_tracker.rb +9 -0
  205. data/lib/landable/traffic/tracker.rb +283 -0
  206. data/lib/landable/traffic/user_tracker.rb +65 -0
  207. data/lib/landable/version.rb +10 -0
  208. data/lib/tasks/landable/cucumber.rake +67 -0
  209. data/lib/tasks/landable/data.rake +166 -0
  210. data/lib/tasks/landable/pgtap.rake +26 -0
  211. data/lib/tasks/landable/rdoc.rake +11 -0
  212. data/lib/tasks/landable/seed.rake +16 -0
  213. data/lib/tasks/landable/spec.rake +15 -0
  214. data/script/cucumber +10 -0
  215. data/spec/concerns/landable/has_assets_spec.rb +75 -0
  216. data/spec/concerns/landable/table_name_spec.rb +15 -0
  217. data/spec/concerns/landable/traffic/table_name_spec.rb +16 -0
  218. data/spec/controllers/concerns/landable/variables_concern_spec.rb +66 -0
  219. data/spec/controllers/landable/api/assets_controller_spec.rb +24 -0
  220. data/spec/controllers/landable/api/categories_controller_spec.rb +45 -0
  221. data/spec/controllers/landable/api/directories_controller_spec.rb +56 -0
  222. data/spec/controllers/landable/api/page_revisions_controller_spec.rb +29 -0
  223. data/spec/controllers/landable/api/pages_controller_spec.rb +271 -0
  224. data/spec/controllers/landable/api_controller_spec.rb +189 -0
  225. data/spec/controllers/public/preview/page_revisions_controller_spec.rb +41 -0
  226. data/spec/controllers/public/preview/pages_controller_spec.rb +36 -0
  227. data/spec/controllers/public/sitemap_controller_spec.rb +25 -0
  228. data/spec/decorators/page_decorator_spec.rb +90 -0
  229. data/spec/dummy/README.rdoc +28 -0
  230. data/spec/dummy/Rakefile +6 -0
  231. data/spec/dummy/app/assets/images/foo.jpg +0 -0
  232. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  233. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  234. data/spec/dummy/app/controllers/application_controller.rb +10 -0
  235. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  236. data/spec/dummy/app/controllers/priority_controller.rb +7 -0
  237. data/spec/dummy/app/helpers/application_helper.rb +5 -0
  238. data/spec/dummy/app/mailers/.keep +0 -0
  239. data/spec/dummy/app/models/.keep +0 -0
  240. data/spec/dummy/app/models/concerns/.keep +0 -0
  241. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  242. data/spec/dummy/app/views/layouts/priority.html.erb +18 -0
  243. data/spec/dummy/app/views/partials/_foobazz.html +1 -0
  244. data/spec/dummy/app/views/priority/show.html.erb +11 -0
  245. data/spec/dummy/bin/bundle +3 -0
  246. data/spec/dummy/bin/rails +4 -0
  247. data/spec/dummy/bin/rake +4 -0
  248. data/spec/dummy/config.ru +4 -0
  249. data/spec/dummy/config/application.rb +21 -0
  250. data/spec/dummy/config/boot.rb +5 -0
  251. data/spec/dummy/config/database.yml +60 -0
  252. data/spec/dummy/config/environment.rb +5 -0
  253. data/spec/dummy/config/environments/development.rb +29 -0
  254. data/spec/dummy/config/environments/production.rb +80 -0
  255. data/spec/dummy/config/environments/test.rb +42 -0
  256. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  257. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  258. data/spec/dummy/config/initializers/inflections.rb +16 -0
  259. data/spec/dummy/config/initializers/landable.rb +22 -0
  260. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  261. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  262. data/spec/dummy/config/initializers/session_store.rb +3 -0
  263. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  264. data/spec/dummy/config/locales/en.yml +23 -0
  265. data/spec/dummy/config/routes.rb +4 -0
  266. data/spec/dummy/db/structure.sql +3736 -0
  267. data/spec/dummy/lib/assets/.keep +0 -0
  268. data/spec/dummy/log/.keep +0 -0
  269. data/spec/dummy/public/404.html +58 -0
  270. data/spec/dummy/public/422.html +58 -0
  271. data/spec/dummy/public/500.html +57 -0
  272. data/spec/dummy/public/favicon.ico +0 -0
  273. data/spec/factories/asset.rb +29 -0
  274. data/spec/factories/authors.rb +12 -0
  275. data/spec/factories/category.rb +6 -0
  276. data/spec/factories/page_revision.rb +6 -0
  277. data/spec/factories/pages.rb +33 -0
  278. data/spec/factories/template.rb +13 -0
  279. data/spec/factories/theme.rb +14 -0
  280. data/spec/fixtures/assets/cthulhu.jpg +0 -0
  281. data/spec/fixtures/assets/panda.png +0 -0
  282. data/spec/fixtures/assets/sloth.png +0 -0
  283. data/spec/fixtures/assets/small.pdf +0 -0
  284. data/spec/helpers/pages_helper_spec.rb +35 -0
  285. data/spec/lib/landable/configuration_spec.rb +20 -0
  286. data/spec/lib/landable/layout_spec.rb +25 -0
  287. data/spec/lib/landable/liquid_spec.rb +24 -0
  288. data/spec/lib/landable/migration_spec.rb +51 -0
  289. data/spec/lib/landable/partial_spec.rb +84 -0
  290. data/spec/lib/landable/tracking_spec.rb +62 -0
  291. data/spec/lib/landable/traffic_spec.rb +45 -0
  292. data/spec/models/landable/access_token_spec.rb +13 -0
  293. data/spec/models/landable/asset_spec.rb +48 -0
  294. data/spec/models/landable/directory_spec.rb +36 -0
  295. data/spec/models/landable/page/errors_spec.rb +30 -0
  296. data/spec/models/landable/page_revision_spec.rb +75 -0
  297. data/spec/models/landable/page_spec.rb +377 -0
  298. data/spec/models/landable/template_spec.rb +47 -0
  299. data/spec/models/landable/theme_spec.rb +8 -0
  300. data/spec/responders/page_render_responder_spec.rb +43 -0
  301. data/spec/routing/public_page_route_spec.rb +36 -0
  302. data/spec/services/landable/authentication_service_spec.rb +61 -0
  303. data/spec/services/landable/render_service_spec.rb +103 -0
  304. data/spec/services/landable/tidy_service_spec.rb +157 -0
  305. data/spec/spec_helper.rb +38 -0
  306. data/spec/support/behaviors.rb +107 -0
  307. data/spec/support/carrier_wave.rb +17 -0
  308. data/spec/support/categories.yml +2 -0
  309. data/spec/support/helpers.rb +22 -0
  310. metadata +795 -0
@@ -0,0 +1,14 @@
1
+ module Landable
2
+ module Traffic
3
+ module TableName
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ model_name = self.model_name.element.pluralize
8
+ schema_name = "#{Landable.configuration.database_schema_prefix}landable_traffic"
9
+ self.table_name = "#{schema_name}.#{model_name}"
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module Landable
2
+ class AccessToken < ActiveRecord::Base
3
+ include Landable::TableName
4
+
5
+ belongs_to :author
6
+ validates_presence_of :author_id
7
+ validates_presence_of :expires_at
8
+
9
+ before_validation do |token|
10
+ token.expires_at ||= 8.hours.from_now
11
+ end
12
+
13
+ scope :fresh, -> { where('expires_at > ?', Time.zone.now) }
14
+ scope :expired, -> { where('expires_at <= ?', Time.zone.now) }
15
+
16
+ def refresh!
17
+ update_column :expires_at, 8.hours.from_now
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ require_dependency 'landable/asset_uploader'
2
+
3
+ require 'carrierwave/orm/activerecord'
4
+ require 'digest/md5'
5
+
6
+ module Landable
7
+ class Asset < ActiveRecord::Base
8
+ include Landable::TableName
9
+
10
+ mount_uploader :data, Landable::AssetUploader
11
+ alias :file :data
12
+ alias :file= :data=
13
+
14
+ # This bit of indirection allows us to generate predictable
15
+ # URLs in the test environment.
16
+ def self.url_generator
17
+ @url_generator ||= proc { |asset| asset.data.try(:url) }
18
+ end
19
+
20
+ belongs_to :author
21
+ has_and_belongs_to_many :pages, join_table: Page.assets_join_table_name
22
+ has_and_belongs_to_many :page_revisions, join_table: PageRevision.assets_join_table_name
23
+ has_and_belongs_to_many :themes, join_table: Theme.assets_join_table_name
24
+
25
+ before_validation :write_metadata, on: :create
26
+
27
+ validates_presence_of :data, :author
28
+ validates_presence_of :name, :mime_type, :md5sum, :file_size
29
+ validates_uniqueness_of :md5sum
30
+ validates_numericality_of :file_size, only_integer: true
31
+ validates_format_of :name, :with => /^[\w\._-]+$/, :on => :create, :multiline => true, :message => 'can only contain alphanumeric characters, periods, underscores, and dashes'
32
+
33
+ def public_url
34
+ self.class.url_generator.call(self)
35
+ end
36
+
37
+ def duplicate_of
38
+ return unless data.present?
39
+ self.class.where(md5sum: calculate_md5sum).first
40
+ end
41
+
42
+ def associated_pages
43
+ paths = []
44
+ Page.where("body like ?", "%#{self.name}%").each do |page|
45
+ paths.push(page.path)
46
+ end
47
+ paths
48
+ end
49
+
50
+ private
51
+
52
+ def calculate_md5sum
53
+ Digest::MD5.hexdigest(data.read) if data.present?
54
+ end
55
+
56
+ def write_metadata
57
+ return unless data.present?
58
+ self.md5sum = calculate_md5sum
59
+ self.mime_type = data.file.content_type.presence || 'application/octet-stream'
60
+ self.file_size = data.file.size
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,20 @@
1
+ require_dependency "landable/search_engine"
2
+ require_dependency "landable/asset"
3
+
4
+ module Landable
5
+ class AssetSearchEngine < SearchEngine
6
+ def initialize(filters)
7
+ super Asset.all, filters
8
+ end
9
+
10
+ def filter_by!(filters)
11
+ if ids = as_array(filters[:ids])
12
+ @scope = @scope.where(asset_id: ids)
13
+ end
14
+
15
+ if name = filters[:name].presence
16
+ @scope = @scope.where('LOWER(name) LIKE ?', "%#{name}%".downcase)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module Landable
2
+ class Author < ActiveRecord::Base
3
+ include Landable::TableName
4
+ has_many :access_tokens
5
+
6
+ def self.authenticate!(username, token_id)
7
+ return unless author = where(username: username).first
8
+ return unless author.access_tokens.fresh.exists?(token_id)
9
+ author
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ module Landable
2
+ class Category < ActiveRecord::Base
3
+ include Landable::TableName
4
+
5
+ has_many :pages
6
+
7
+ validates_uniqueness_of :name, case_sensitive: false
8
+ validates_uniqueness_of :slug
9
+
10
+ before_validation :set_slug
11
+
12
+ def to_liquid
13
+ {
14
+ 'name' => name,
15
+ 'pages' => pages.published,
16
+ }
17
+ end
18
+
19
+
20
+ protected
21
+
22
+ def set_slug
23
+ self.slug = name.downcase.gsub(/[^\w]/, '_').gsub(/_{2,}/, '_')
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Landable
2
+ class Directory
3
+ include ActiveModel::SerializerSupport
4
+
5
+ def self.listing(parent)
6
+ parent_with_slash = parent.gsub(/^(.*?)\/?$/, '\1/')
7
+ pages = Page.where('path LIKE ?', "#{parent_with_slash}%").to_a
8
+ subdirs = pages.group_by { |page| page.directory_after(parent_with_slash) }
9
+ notdirs = subdirs.delete(nil) || []
10
+ subdirs = subdirs.map { |name, contents| Directory.new("#{parent}/#{name}") }
11
+ Directory.new(parent, subdirs.sort_by(&:path), notdirs.sort_by(&:path))
12
+ end
13
+
14
+ attr_reader :path, :subdirectories, :pages
15
+ alias_attribute :id, :path
16
+
17
+ def initialize(path, subdirectories = [], pages = [])
18
+ @path = path.squeeze '/'
19
+ @subdirectories = subdirectories
20
+ @pages = pages
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,259 @@
1
+ require_dependency 'landable/theme'
2
+ require_dependency 'landable/page_revision'
3
+ require_dependency 'landable/category'
4
+ require_dependency 'landable/has_assets'
5
+ require_dependency 'landable/author'
6
+
7
+ module Landable
8
+ class Page < ActiveRecord::Base
9
+ include ActionView::Helpers::TagHelper
10
+ include Landable::HasAssets
11
+ include Landable::Engine.routes.url_helpers
12
+ include Landable::TableName
13
+
14
+ validates_presence_of :path, :status_code
15
+ validates_presence_of :redirect_url, if: -> page { page.redirect? }
16
+
17
+ validates_inclusion_of :status_code, in: [200, 301, 302, 410]
18
+
19
+ validates_with PathValidator, fields: [:path]
20
+ validates_uniqueness_of :path
21
+ validates :path, presence: true
22
+
23
+ validate :forbid_changing_path, on: :update
24
+
25
+ validate :body_strip_search
26
+ validates :redirect_url, url: true, allow_blank: true
27
+ validate :hero_asset_existence
28
+
29
+ belongs_to :theme, class_name: 'Landable::Theme', inverse_of: :pages
30
+ belongs_to :published_revision, class_name: 'Landable::PageRevision'
31
+ belongs_to :category, class_name: 'Landable::Category'
32
+ belongs_to :updated_by_author, class_name: 'Landable::Author'
33
+ belongs_to :hero_asset, class_name: 'Landable::Asset'
34
+ has_many :revisions, class_name: 'Landable::PageRevision'
35
+ has_many :screenshots, class_name: 'Landable::Screenshot', as: :screenshotable
36
+
37
+ scope :imported, -> { where("imported_at IS NOT NULL") }
38
+ scope :sitemappable, -> { where("COALESCE(meta_tags -> 'robots' NOT LIKE '%noindex%', TRUE)")
39
+ .where(status_code: 200)}
40
+ scope :published, -> { where("published_revision_id is NOT NULL") }
41
+
42
+ before_validation :downcase_path!
43
+
44
+ before_save -> page {
45
+ page.lock_version ||= 0
46
+ page.is_publishable = true unless page.published_revision_id_changed?
47
+ }
48
+
49
+ class << self
50
+ def missing
51
+ new(status_code: 410)
52
+ end
53
+
54
+ def by_path(path)
55
+ where(path: path).first || missing
56
+ end
57
+
58
+ def by_path!(path)
59
+ where(path: path).first!
60
+ end
61
+
62
+ def with_fuzzy_path(path)
63
+ select("*, similarity(path, #{Page.sanitize path}) _sml").
64
+ where('path LIKE ?', "%#{path}%").
65
+ order('_sml DESC, path ASC')
66
+ end
67
+
68
+ def example(attrs)
69
+ defaults = {
70
+ title: 'Example page',
71
+ body: '<div>Example page contents would live here</div>'
72
+ }
73
+
74
+ new defaults.merge(attrs)
75
+ end
76
+
77
+
78
+ def generate_sitemap(options = {})
79
+ pages = Landable::Page.sitemappable
80
+ xml = Builder::XmlMarkup.new( :indent => 2 )
81
+ xml.instruct! :xml, encoding: "UTF-8"
82
+ xml.urlset(xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9") do |xml|
83
+ pages.each do |page|
84
+ next if options[:exclude_categories].to_a.include? page.category.try(:name)
85
+ xml.url do |p|
86
+ p.loc "#{options[:protocol]}://#{options[:host]}#{page.path}"
87
+ p.lastmod page.updated_at.to_time.iso8601
88
+ p.changefreq 'weekly'
89
+ p.priority '1'
90
+ end
91
+ end
92
+
93
+ if options[:sitemap_additional_paths].present?
94
+ options[:sitemap_additional_paths].each do |page|
95
+ xml.url do |p|
96
+ p.loc "#{options[:protocol]}://#{options[:host]}#{page}"
97
+ p.changefreq 'weekly'
98
+ p.priority '1'
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def downcase_path!
107
+ path.try :downcase!
108
+ end
109
+
110
+ def path_extension
111
+ path.match(/\.(\w{2,})$/).try(:[], 1) if path
112
+ end
113
+
114
+ def content_type
115
+ case path_extension
116
+ when nil, 'htm', 'html'
117
+ 'text/html'
118
+ when 'json'
119
+ 'application/json'
120
+ when 'xml'
121
+ 'application/xml'
122
+ else
123
+ 'text/plain'
124
+ end
125
+ end
126
+
127
+ def html?
128
+ content_type == 'text/html'
129
+ end
130
+
131
+ def directory_after(prefix)
132
+ remainder = path.gsub(/^#{prefix}\/?/, '')
133
+ segments = remainder.split('/', 2)
134
+ if segments.length == 1
135
+ nil
136
+ else
137
+ segments.first
138
+ end
139
+ end
140
+
141
+ def redirect?
142
+ status_code == 301 || status_code == 302
143
+ end
144
+
145
+ def path=(name)
146
+ # if not present, add a leading slash for a non-empty path
147
+ if name and not name.empty?
148
+ name = name.gsub(/^\/?(.*)/, '/\1')
149
+ end
150
+
151
+ self[:path] = name
152
+ end
153
+
154
+ def hero_asset_name
155
+ self.hero_asset.try(:name)
156
+ end
157
+
158
+ def hero_asset_name=(name)
159
+ @hero_asset_name = name
160
+ asset = Asset.find_by_name(name)
161
+ self.hero_asset_id = asset.try(:asset_id)
162
+ end
163
+
164
+ def hero_asset_url
165
+ self.hero_asset.try(:public_url)
166
+ end
167
+
168
+ def publish!(options)
169
+ transaction do
170
+ published_revision.unpublish! if published_revision
171
+ revision = revisions.create! options
172
+ update_attributes!(published_revision: revision, is_publishable: false)
173
+ end
174
+ end
175
+
176
+ def published?
177
+ published_revision.present?
178
+ end
179
+
180
+ def revert_to!(revision)
181
+ self.title = revision.title
182
+ self.path = revision.path
183
+ self.body = revision.body
184
+ self.head_content = revision.head_content
185
+ self.category_id = revision.category_id
186
+ self.theme_id = revision.theme_id
187
+ self.status_code = revision.status_code
188
+ self.meta_tags = revision.meta_tags
189
+ self.redirect_url = revision.redirect_url
190
+
191
+ save!
192
+ end
193
+
194
+ def preview_path
195
+ public_preview_page_path(self)
196
+ end
197
+
198
+ def preview_url
199
+ public_preview_page_url(self)
200
+ end
201
+
202
+ def forbid_changing_path
203
+ errors[:path] = "can not be changed!" if self.path_changed?
204
+ end
205
+
206
+ def body_strip_search
207
+ begin
208
+ RenderService.call(self)
209
+ rescue ::Liquid::Error => error
210
+ errors[:body] = 'contains a Liquid syntax error'
211
+ rescue StandardError => error
212
+ errors[:body] = 'had a problem: ' + error.message
213
+ end
214
+ end
215
+
216
+ def hero_asset_existence
217
+ return true if @hero_asset_name.blank?
218
+ unless Asset.find_by_name(@hero_asset_name)
219
+ errors[:hero_asset_name] = "System can't find an asset with this name"
220
+ end
221
+ end
222
+
223
+ def to_liquid
224
+ {
225
+ "title" => title,
226
+ "url" => path,
227
+ "hero_asset" => hero_asset ? true : false,
228
+ "hero_asset_url" => hero_asset_url,
229
+ "abstract" => abstract
230
+ }
231
+ end
232
+
233
+ module Errors
234
+ extend ActiveSupport::Concern
235
+
236
+ class GoneError < Error
237
+ STATUS_CODE = 410
238
+ end
239
+
240
+ def error?
241
+ (400..599).cover? status_code
242
+ end
243
+
244
+ def error
245
+ return nil unless error?
246
+
247
+ case status_code
248
+ when 410
249
+ GoneError.new
250
+ else
251
+ Landable::Error.new "Missing a Page error class for #{status_code}"
252
+ end
253
+ end
254
+ end
255
+
256
+ include Errors
257
+
258
+ end
259
+ end