integral 1.3.0 → 1.4.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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -30
  3. data/Rakefile +1 -1
  4. data/app/assets/images/integral/defaults/no_image_available.jpg +0 -0
  5. data/app/assets/javascripts/integral/backend.js +102 -11
  6. data/app/assets/javascripts/integral/frontend.js +37 -0
  7. data/app/assets/javascripts/integral/support/confirm_modal.coffee +2 -2
  8. data/app/assets/javascripts/integral/support/gallery.coffee +71 -54
  9. data/app/assets/javascripts/integral/support/lib/lazysizes.js +755 -0
  10. data/app/assets/javascripts/integral/support/lib/materialize-tags.js +49 -44
  11. data/app/assets/javascripts/integral/support/ls.instagram.js +57 -0
  12. data/app/assets/javascripts/integral/support/ls.twitter.js +66 -0
  13. data/app/assets/javascripts/integral/support/record_selector.coffee +1 -1
  14. data/app/assets/javascripts/integral/support/remote_form.coffee +5 -2
  15. data/app/assets/stylesheets/integral/backend.sass +2 -1
  16. data/app/assets/stylesheets/integral/backend/_foundation_settings.scss +3 -4
  17. data/app/assets/stylesheets/integral/backend/dashboard-layout.scss +4 -1
  18. data/app/assets/stylesheets/integral/backend/materialize-tags.sass +1 -1
  19. data/app/assets/stylesheets/integral/backend/modules/timeline.scss +214 -0
  20. data/app/assets/stylesheets/integral/backend/shared.sass +80 -11
  21. data/app/assets/stylesheets/integral/frontend.scss +45 -0
  22. data/app/assets/stylesheets/integral/frontend/_foundation_settings.scss +2 -2
  23. data/app/assets/stylesheets/integral/frontend/blog.sass +155 -142
  24. data/app/assets/stylesheets/integral/frontend/layout.sass +3 -3
  25. data/app/assets/stylesheets/integral/frontend/modules/article-footer.scss +55 -0
  26. data/app/assets/stylesheets/integral/frontend/modules/article.scss +34 -0
  27. data/app/assets/stylesheets/integral/frontend/modules/horizontal-post.scss +44 -0
  28. data/app/assets/stylesheets/integral/frontend/modules/inline-articles.scss +23 -0
  29. data/app/assets/stylesheets/integral/frontend/modules/latest-post.scss +37 -0
  30. data/app/assets/stylesheets/integral/frontend/modules/list-widget.scss +50 -0
  31. data/app/assets/stylesheets/integral/frontend/modules/piped-list.scss +33 -0
  32. data/app/assets/stylesheets/integral/frontend/modules/post-tags.scss +19 -0
  33. data/app/assets/stylesheets/integral/frontend/modules/scroll-container.scss +9 -0
  34. data/app/assets/stylesheets/integral/frontend/modules/sidebar-articles.scss +42 -0
  35. data/app/assets/stylesheets/integral/frontend/modules/sidebar-tags.scss +6 -0
  36. data/app/assets/stylesheets/integral/frontend/modules/sidebar-widget.scss +47 -0
  37. data/app/assets/stylesheets/integral/frontend/modules/vertical-post.scss +31 -0
  38. data/app/assets/stylesheets/integral/frontend/share_modal.sass +0 -5
  39. data/app/assets/stylesheets/integral/support/gallery.sass +8 -0
  40. data/app/assets/stylesheets/integral/support/media-query-indicator.sass +6 -0
  41. data/app/controllers/integral/application_controller.rb +7 -1
  42. data/app/controllers/integral/backend/activities_controller.rb +13 -2
  43. data/app/controllers/integral/backend/base_controller.rb +60 -7
  44. data/app/controllers/integral/backend/categories_controller.rb +49 -0
  45. data/app/controllers/integral/backend/pages_controller.rb +7 -2
  46. data/app/controllers/integral/backend/posts_controller.rb +8 -3
  47. data/app/controllers/integral/backend/static_pages_controller.rb +4 -0
  48. data/app/controllers/integral/backend/users_controller.rb +13 -7
  49. data/app/controllers/integral/categories_controller.rb +31 -0
  50. data/app/controllers/integral/pages_controller.rb +1 -1
  51. data/app/controllers/integral/posts_controller.rb +5 -3
  52. data/app/decorators/integral/category_decorator.rb +30 -0
  53. data/app/decorators/integral/category_version_decorator.rb +7 -0
  54. data/app/decorators/integral/image_version_decorator.rb +7 -0
  55. data/app/decorators/integral/list_decorator.rb +1 -1
  56. data/app/decorators/integral/list_version_decorator.rb +7 -0
  57. data/app/decorators/integral/page_version_decorator.rb +7 -0
  58. data/app/decorators/integral/post_decorator.rb +9 -1
  59. data/app/decorators/integral/post_version_decorator.rb +7 -0
  60. data/app/decorators/integral/user_decorator.rb +1 -1
  61. data/app/decorators/integral/user_version_decorator.rb +7 -0
  62. data/app/decorators/integral/version_decorator.rb +51 -12
  63. data/app/helpers/integral/backend/base_helper.rb +56 -2
  64. data/app/helpers/integral/blog_helper.rb +21 -4
  65. data/app/jobs/integral/webhook/delivery_job.rb +37 -0
  66. data/app/mailers/integral/contact_mailer.rb +4 -1
  67. data/app/models/concerns/integral/lazy_contentable.rb +54 -0
  68. data/app/models/concerns/integral/webhook/delivery.rb +30 -0
  69. data/app/models/concerns/integral/webhook/observable.rb +23 -0
  70. data/app/models/integral/category.rb +20 -0
  71. data/app/models/integral/category_version.rb +8 -0
  72. data/app/models/integral/list_item.rb +1 -2
  73. data/app/models/integral/page.rb +18 -3
  74. data/app/models/integral/post.rb +28 -1
  75. data/app/models/integral/version.rb +2 -2
  76. data/app/models/integral/webhook/endpoint.rb +40 -0
  77. data/app/models/integral/webhook/event.rb +20 -0
  78. data/app/policies/integral/base_policy.rb +1 -0
  79. data/app/policies/integral/category_policy.rb +9 -0
  80. data/app/serializers/integral/post_serializer.rb +24 -0
  81. data/app/uploaders/integral/avatar_uploader.rb +1 -1
  82. data/app/views/integral/backend/activities/_activity.haml +21 -0
  83. data/app/views/integral/backend/activities/_grid.haml +1 -2
  84. data/app/views/integral/backend/activities/shared/_grid.haml +3 -2
  85. data/app/views/integral/backend/activities/shared/{_listing.haml → index.haml} +1 -0
  86. data/app/views/integral/backend/activities/shared/{_log.haml → show.haml} +0 -0
  87. data/app/views/integral/backend/categories/_modal.haml +25 -0
  88. data/app/views/integral/backend/lists/_child_fields.haml +1 -1
  89. data/app/views/integral/backend/lists/_item_container.haml +1 -1
  90. data/app/views/integral/backend/lists/_item_modal.haml +1 -1
  91. data/app/views/integral/backend/lists/_list_item_fields.haml +1 -1
  92. data/app/views/integral/backend/pages/_form.haml +1 -4
  93. data/app/views/integral/backend/pages/_grid.haml +34 -9
  94. data/app/views/integral/backend/pages/edit.haml +9 -3
  95. data/app/views/integral/backend/pages/index.haml +11 -21
  96. data/app/views/integral/backend/pages/list.haml +22 -0
  97. data/app/views/integral/backend/pages/show.haml +48 -0
  98. data/app/views/integral/backend/posts/_form.haml +8 -6
  99. data/app/views/integral/backend/posts/_grid.haml +33 -7
  100. data/app/views/integral/backend/posts/index.haml +13 -19
  101. data/app/views/integral/backend/posts/list.haml +20 -0
  102. data/app/views/integral/backend/posts/show.haml +54 -0
  103. data/app/views/integral/backend/shared/_activity_modal.haml +13 -0
  104. data/app/views/integral/backend/shared/cards/_categories.haml +34 -0
  105. data/app/views/integral/backend/{static_pages/_card.haml → shared/cards/_object.haml} +0 -0
  106. data/app/views/integral/backend/shared/cards/_recent_activity.haml +20 -0
  107. data/app/views/integral/backend/shared/cards/_recent_pages.haml +19 -0
  108. data/app/views/integral/backend/shared/cards/_recent_posts.haml +18 -0
  109. data/app/views/integral/backend/shared/cards/_recent_user_activity.haml +1 -0
  110. data/app/views/integral/backend/shared/cards/_recent_users.haml +19 -0
  111. data/app/views/integral/backend/shared/cards/_top_post_authors.haml +19 -0
  112. data/app/views/integral/backend/shared/record_selector/_record.haml +6 -4
  113. data/app/views/integral/backend/static_pages/dashboard.haml +13 -11
  114. data/app/views/integral/backend/users/_grid.haml +24 -7
  115. data/app/views/integral/backend/users/index.haml +11 -17
  116. data/app/views/integral/backend/users/list.haml +18 -0
  117. data/app/views/integral/backend/users/show.haml +5 -11
  118. data/app/views/integral/categories/show.haml +5 -0
  119. data/app/views/integral/posts/_article_footer.haml +17 -0
  120. data/app/views/integral/posts/_card.haml +11 -0
  121. data/app/views/integral/posts/_latest_post.haml +8 -0
  122. data/app/views/integral/posts/_most_read_section.haml +8 -0
  123. data/app/views/integral/posts/_post.haml +11 -0
  124. data/app/views/integral/posts/_similar_posts.haml +5 -0
  125. data/app/views/integral/posts/index.haml +6 -5
  126. data/app/views/integral/posts/templates/default.haml +34 -33
  127. data/app/views/integral/shared/_subscribe_modal.haml +14 -0
  128. data/app/views/integral/shared/blog/_categories.haml +15 -0
  129. data/app/views/integral/shared/blog/_layout.haml +9 -0
  130. data/app/views/integral/shared/blog/_sidebar.haml +10 -0
  131. data/app/views/integral/shared/gallery/_placeholder.haml +1 -1
  132. data/app/views/integral/shared/gallery/_slide.haml +2 -2
  133. data/app/views/integral/shared/gallery/gallery.haml +5 -2
  134. data/app/views/integral/shared/sidebar/_item.haml +8 -0
  135. data/app/views/integral/shared/sidebar/_newsletter_signup.haml +7 -0
  136. data/app/views/integral/shared/sidebar/_popular_posts.haml +7 -0
  137. data/app/views/integral/shared/sidebar/_popular_tags.haml +7 -0
  138. data/app/views/integral/shared/sidebar/_recent_posts.haml +7 -0
  139. data/app/views/integral/tags/index.haml +2 -2
  140. data/app/views/integral/tags/show.haml +3 -6
  141. data/app/views/layouts/integral/backend.html.haml +3 -0
  142. data/app/views/layouts/integral/backend/_main_menu_items.haml +10 -0
  143. data/app/views/layouts/integral/frontend.html.haml +3 -3
  144. data/config/locales/en.yml +52 -49
  145. data/db/migrate/20190414172018_create_webhook_endpoints.rb +10 -0
  146. data/db/migrate/20190929191412_add_integral_post_categories.rb +13 -0
  147. data/db/migrate/20191203090008_add_image_to_integral_categories.rb +6 -0
  148. data/db/migrate/20200401210442_create_category_versions.rb +20 -0
  149. data/db/seeds.rb +3 -1
  150. data/lib/generators/integral/assets_generator.rb +2 -2
  151. data/lib/generators/integral/install_generator.rb +1 -1
  152. data/lib/generators/integral/views_generator.rb +1 -1
  153. data/lib/generators/templates/integral.rb +5 -0
  154. data/lib/integral.rb +3 -30
  155. data/lib/integral/acts_as_listable.rb +2 -2
  156. data/lib/integral/chart_renderer/base.rb +2 -0
  157. data/lib/integral/content_renderer.rb +2 -2
  158. data/lib/integral/engine.rb +2 -2
  159. data/lib/integral/grids/activities_grid.rb +15 -1
  160. data/lib/integral/list_item_renderer.rb +4 -2
  161. data/lib/integral/list_renderer.rb +1 -0
  162. data/lib/integral/middleware/page_router.rb +15 -6
  163. data/lib/integral/router.rb +19 -3
  164. data/lib/integral/version.rb +1 -1
  165. data/lib/integral/widgets/swiper_list.rb +3 -2
  166. data/public/images/integral/demo/continous-integration.png +0 -0
  167. data/public/images/integral/demo/foundation-frontend-framework.jpg +0 -0
  168. data/public/images/integral/demo/heroku.png +0 -0
  169. data/public/images/integral/demo/integral-cms-without-hassle.jpg +0 -0
  170. data/public/images/integral/demo/integral-features-activity-tracking.jpg +0 -0
  171. data/public/images/integral/demo/integral-features-contact-form.png +0 -0
  172. data/public/images/integral/demo/integral-features-design.jpg +0 -0
  173. data/public/images/integral/demo/integral-features-dynamic-pages.jpg +0 -0
  174. data/public/images/integral/demo/integral-features-image-management.jpg +0 -0
  175. data/public/images/integral/demo/integral-features-integrated-blog.jpg +0 -0
  176. data/public/images/integral/demo/integral-features-list-management.jpg +0 -0
  177. data/public/images/integral/demo/integral-features-seo-ready.jpg +0 -0
  178. data/public/images/integral/demo/integral-features-user-management.jpg +0 -0
  179. data/public/images/integral/demo/integral-presentation.png +0 -0
  180. data/spec/factories.rb +15 -7
  181. metadata +110 -98
  182. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/copywidget.png +0 -0
  183. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/editwidget.png +0 -0
  184. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/copywidget.png +0 -0
  185. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/editwidget.png +0 -0
  186. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/removewidget.png +0 -0
  187. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/widget.png +0 -0
  188. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/removewidget.png +0 -0
  189. data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/widget.png +0 -0
  190. data/app/assets/javascripts/ckeditor/plugins/integral-card/plugin.js +0 -86
  191. data/app/assets/javascripts/ckeditor/plugins/integralrecentposts/dialogs/integralrecentposts.js +0 -40
  192. data/app/assets/javascripts/ckeditor/plugins/integralrecentposts/plugin.js +0 -32
  193. data/app/assets/javascripts/ckeditor/plugins/numericinput/LICENSE.md +0 -363
  194. data/app/assets/javascripts/ckeditor/plugins/numericinput/README.md +0 -16
  195. data/app/assets/javascripts/ckeditor/plugins/numericinput/plugin.js +0 -354
  196. data/app/assets/stylesheets/integral/frontend.sass +0 -25
  197. data/app/views/integral/backend/pages/activities.haml +0 -2
  198. data/app/views/integral/backend/pages/activity.haml +0 -1
  199. data/app/views/integral/backend/posts/activities.haml +0 -3
  200. data/app/views/integral/backend/posts/activity.haml +0 -1
  201. data/app/views/integral/posts/_collection.haml +0 -4
  202. data/app/views/integral/posts/_item.haml +0 -16
  203. data/app/views/integral/shared/_blog_layout.haml +0 -15
  204. data/app/views/integral/shared/_blog_sidebar.haml +0 -49
  205. data/lib/integral/slack_bot.rb +0 -45
@@ -0,0 +1,54 @@
1
+ # Adds lazy content behaviour to a model
2
+ module Integral
3
+ # Enable lazy loading WYSIWYG content
4
+ module LazyContentable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_save :lazyload_content
9
+ end
10
+
11
+ # @return [String] body HTML ready for WYSIWYG editor
12
+ def editor_body
13
+ html = Nokogiri::HTML(body)
14
+
15
+ # Remove image lazyloading
16
+ html.css('img.lazyload').each do |element|
17
+ element.attributes['src'].value = element.attributes['data-src'].value
18
+ end
19
+
20
+ html.css('body').inner_html
21
+ end
22
+
23
+ private
24
+
25
+ def lazyload_content
26
+ html = Nokogiri::HTML(body)
27
+
28
+ # Add lazyloading to tagged images
29
+ html.css('img.lazyload').each do |element|
30
+ element['data-src'] = element.attributes['src'].value
31
+ element.attributes['src'].value = ''
32
+ end
33
+
34
+ # Add lazy loading to oEmbeds
35
+ html.css('div[data-oembed-url]').each do |element|
36
+ next if element.css('blockquote.lazyload').any?
37
+
38
+ blockquote = element.css('blockquote').first
39
+
40
+ next unless blockquote.present?
41
+
42
+ blockquote['class'] = "#{blockquote.attributes['class']} lazyload"
43
+
44
+ if element['data-oembed-url'].starts_with?('https://www.instagram.com')
45
+ blockquote['data-instagram'] = 'instagram'
46
+ elsif element['data-oembed-url'].starts_with?('https://twitter.com')
47
+ blockquote['data-twitter'] = 'twitter'
48
+ end
49
+ end
50
+
51
+ self.body = html.css('body').inner_html
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,30 @@
1
+ module Integral
2
+ module Webhook
3
+ # Helper to handle the delivery of webhook events
4
+ module Delivery
5
+ extend ActiveSupport::Concern
6
+
7
+ # Abstract method to be overriden
8
+ #
9
+ # @return [Hash] which repesents the object
10
+ def webhook_payload
11
+ {}
12
+ end
13
+
14
+ # Build event_name and delivery webhook
15
+ def deliver_webhook(action)
16
+ event_name = "#{self.class.name.underscore}_#{action}"
17
+ deliver_webhook_event(event_name, webhook_payload)
18
+ end
19
+
20
+ # Create webhook event and deliver it to any endpoint listening
21
+ def deliver_webhook_event(event_name, payload)
22
+ event = Webhook::Event.new(event_name, payload || {})
23
+
24
+ Endpoint.for_event(event_name).each do |endpoint|
25
+ endpoint.deliver(event)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ module Integral
2
+ module Webhook
3
+ # Helper to deliver webhooks for each of the 3 major model lifestyle events - creation, update and deletion
4
+ module Observable
5
+ extend ActiveSupport::Concern
6
+ include Webhook::Delivery
7
+
8
+ included do
9
+ after_commit on: :create do
10
+ deliver_webhook(:created)
11
+ end
12
+
13
+ after_commit on: :update do
14
+ deliver_webhook(:updated)
15
+ end
16
+
17
+ after_commit on: :destroy do
18
+ deliver_webhook(:destroyed)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module Integral
2
+ # Represents a user post category
3
+ class Category < ApplicationRecord
4
+ has_paper_trail class_name: 'Integral::CategoryVersion'
5
+
6
+ # Slugging
7
+ extend FriendlyId
8
+ friendly_id :title
9
+
10
+ # Associations
11
+ has_many :posts # TODO: Touch the posts on change
12
+ belongs_to :image, class_name: 'Integral::Image', optional: true
13
+
14
+ # Validations
15
+ validates :slug, presence: true
16
+ validates_format_of :slug, with: /\A[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\z/
17
+ validates :title, presence: true, length: { minimum: 4, maximum: 60 }
18
+ validates :description, presence: true, length: { minimum: 25, maximum: 300 }
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ # Integral namespace
2
+ module Integral
3
+ # Record PaperTrail of Integral::Category
4
+ class CategoryVersion < Version
5
+ self.table_name = :integral_category_versions
6
+ self.sequence_name = :integral_category_versions_id_seq
7
+ end
8
+ end
@@ -2,14 +2,13 @@ module Integral
2
2
  # Represents an item within a particular list
3
3
  class ListItem < ApplicationRecord
4
4
  after_initialize :set_defaults
5
- before_save :touch_list
6
5
  after_touch :touch_list
7
6
 
8
7
  # Default scope orders by priority and includes children
9
8
  default_scope { includes(:children).includes(:image).order(:priority) }
10
9
 
11
10
  # Associations
12
- belongs_to :list, optional: true
11
+ belongs_to :list, optional: true, touch: true
13
12
  belongs_to :image, optional: true
14
13
  has_and_belongs_to_many(:children,
15
14
  -> { order(:priority) },
@@ -1,6 +1,8 @@
1
1
  module Integral
2
2
  # Represents a public viewable page
3
3
  class Page < ApplicationRecord
4
+ include LazyContentable
5
+
4
6
  acts_as_paranoid # Soft-deletion
5
7
  acts_as_listable # Listable Item
6
8
 
@@ -12,9 +14,9 @@ module Integral
12
14
  # /foo, /foo/bar, /123/456
13
15
  # Bad:
14
16
  # //, foo, /foo bar, /foo?y=123, /foo$
15
- PATH_REGEX = /\A\/[\/.a-zA-Z0-9-]+\z/
17
+ PATH_REGEX = %r{\A/[/.a-zA-Z0-9-]+\z}.freeze
16
18
 
17
- enum status: %i[draft published]
19
+ enum status: %i[draft published archived]
18
20
 
19
21
  # Associations
20
22
  belongs_to :parent, class_name: 'Integral::Page', optional: true
@@ -31,8 +33,13 @@ module Integral
31
33
  validate :validate_path_is_not_black_listed
32
34
  validate :validate_parent_is_available
33
35
 
36
+ # Callbacks
37
+ before_save :set_paper_trail_event
38
+
34
39
  # Scopes
35
40
  scope :search, ->(query) { where('lower(title) LIKE ? OR lower(path) LIKE ?', "%#{query.downcase}%", "%#{query.downcase}%") }
41
+ # TODO: Remove this on Rails 6 upgrade
42
+ scope :not_archived, -> { where.not(status: :archived) }
36
43
 
37
44
  # Return all available parents
38
45
  # TODO: Update parent behaviour
@@ -123,11 +130,18 @@ module Integral
123
130
 
124
131
  private
125
132
 
133
+ def set_paper_trail_event
134
+ if persisted? && published? && status_changed?
135
+ self.paper_trail_event = :publish
136
+ end
137
+ end
138
+
126
139
  # @return [Array] containing available human readable statuses against there numeric value
127
140
  def self.available_statuses
128
141
  [
129
142
  ['Draft', 0],
130
- ['Published', 1]
143
+ ['Published', 1],
144
+ ['Archived', 2]
131
145
  ]
132
146
  end
133
147
 
@@ -142,6 +156,7 @@ module Integral
142
156
 
143
157
  Integral.black_listed_paths.each do |black_listed_path|
144
158
  next unless path&.starts_with?(black_listed_path)
159
+
145
160
  valid = false
146
161
  errors.add(:path, 'Invalid path')
147
162
  break
@@ -2,6 +2,9 @@ module Integral
2
2
  # Represents a user post
3
3
  class Post < ApplicationRecord
4
4
  include ActionView::Helpers::DateHelper
5
+ include LazyContentable
6
+ include Webhook::Observable
7
+
5
8
  acts_as_paranoid # Soft-deletion
6
9
  acts_as_listable if Integral.blog_enabled? # Listable Item
7
10
  acts_as_taggable # Tagging
@@ -19,6 +22,7 @@ module Integral
19
22
 
20
23
  # Associations
21
24
  belongs_to :user
25
+ belongs_to :category
22
26
  belongs_to :image, class_name: 'Integral::Image', optional: true
23
27
  belongs_to :preview_image, class_name: 'Integral::Image', optional: true
24
28
 
@@ -29,7 +33,10 @@ module Integral
29
33
 
30
34
  # Callbacks
31
35
  before_save :set_published_at
36
+ before_save :set_paper_trail_event
32
37
  before_save :set_tags_context
38
+ after_update :deliver_published_webhook_on_update
39
+ after_create :deliver_published_webhook_on_create
33
40
 
34
41
  # Aliases
35
42
  alias author user
@@ -111,6 +118,10 @@ module Integral
111
118
 
112
119
  private
113
120
 
121
+ def webhook_payload
122
+ Integral::PostSerializer.new(self).serializable_hash
123
+ end
124
+
114
125
  def set_slug
115
126
  if slug_changed? && Post.exists_by_friendly_id?(slug)
116
127
  self.slug = resolve_friendly_id_conflict([slug])
@@ -118,7 +129,15 @@ module Integral
118
129
  end
119
130
 
120
131
  def set_published_at
121
- self.published_at = Time.now if published? && published_at.nil?
132
+ if published? && published_at.nil?
133
+ self.published_at = Time.now
134
+ end
135
+ end
136
+
137
+ def set_paper_trail_event
138
+ if persisted? && published? && status_changed?
139
+ self.paper_trail_event = :publish
140
+ end
122
141
  end
123
142
 
124
143
  # Set the context of tags so that draft and archived tags are not displayed publicly
@@ -145,5 +164,13 @@ module Integral
145
164
  contexts.delete(tag_context)
146
165
  contexts
147
166
  end
167
+
168
+ def deliver_published_webhook_on_update
169
+ deliver_webhook(:published) if status_changed? && published?
170
+ end
171
+
172
+ def deliver_published_webhook_on_create
173
+ deliver_webhook(:published) if published?
174
+ end
148
175
  end
149
176
  end
@@ -10,7 +10,7 @@ module Integral
10
10
  def self.available_actions
11
11
  available = []
12
12
 
13
- %w[update create destroy].each do |item|
13
+ %w[update create destroy publish].each do |item|
14
14
  available << [I18n.t("integral.actions.#{item}"), item]
15
15
  end
16
16
 
@@ -21,7 +21,7 @@ module Integral
21
21
  def self.available_objects
22
22
  available = []
23
23
 
24
- [Integral::Post, Integral::Page, Integral::List, Integral::Image, Integral::User].each do |item|
24
+ [Integral::Post, Integral::Category, Integral::Page, Integral::List, Integral::Image, Integral::User].concat(Integral.additional_tracked_classes).each do |item|
25
25
  available << [item.model_name.human, item]
26
26
  end
27
27
 
@@ -0,0 +1,40 @@
1
+ module Integral
2
+ # Webhook Implementation - https://benediktdeicke.com/2017/09/sending-webhooks-with-rails/
3
+ module Webhook
4
+ # A Webhook::Endpoint can listen to one or more Webhook::Event, everytime an Event is created
5
+ # it is sent (along with its payload) to the Endpoint target_url
6
+ class Endpoint < ApplicationRecord
7
+ self.table_name = 'integral_webhook_endpoints'
8
+
9
+ attribute :events, :string, array: true, default: []
10
+
11
+ validates :target_url,
12
+ presence: true,
13
+ format: URI.regexp(%w[http https])
14
+
15
+ validates :events,
16
+ presence: true
17
+
18
+ # @param events [Array] list of Integral::Webhook::Event names
19
+ #
20
+ # @return [Integral::Webhook::Endpoint] endpoints which are listening for the provided events
21
+ def self.for_event(events)
22
+ where('events @> ARRAY[?]::varchar[]', Array(events))
23
+ end
24
+
25
+ # Override the default setter to normalise the events and validate them (TODO)
26
+ def events=(events)
27
+ events = Array(events).map { |event| event.to_s.underscore }
28
+ # TODO: Validate events from specified list in Integral.config which can be used to
29
+ # list them on the backend for click and create
30
+ # super(Webhook::Event::EVENT_TYPES & events)
31
+ super(events)
32
+ end
33
+
34
+ # Deliver a particular event to the endpoint
35
+ def deliver(event)
36
+ Webhook::DeliveryJob.perform_later(id, event.to_json)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module Integral
2
+ # Webhook Implementation source from - https://benediktdeicke.com/2017/09/sending-webhooks-with-rails/
3
+ module Webhook
4
+ # An event instance which a Webhook::Endpoint might be listening too, for example a post publication, creation or deletion
5
+ class Event
6
+ attr_reader :event_name, :payload
7
+
8
+ def initialize(event_name, payload = {})
9
+ @event_name = event_name
10
+ @payload = payload
11
+ end
12
+
13
+ # Adds the event_name to the JSON representation
14
+ def as_json(*_args)
15
+ payload[:event_name] = event_name
16
+ payload
17
+ end
18
+ end
19
+ end
20
+ end
@@ -39,6 +39,7 @@ module Integral
39
39
 
40
40
  alias destroy? manager?
41
41
  alias index? manager?
42
+ alias list? manager?
42
43
  alias show? manager?
43
44
  alias new? manager?
44
45
  alias create? manager?
@@ -0,0 +1,9 @@
1
+ module Integral
2
+ # Handles Category authorization
3
+ class CategoryPolicy < BasePolicy
4
+ # @return [Symbol] role name
5
+ def role_name
6
+ :post_manager
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module Integral
2
+ # Used too transform Integral::Post records into JSON format
3
+ class PostSerializer
4
+ include FastJsonapi::ObjectSerializer
5
+
6
+ attributes :title, :description, :status, :slug, :created_at, :updated_at, :published_at, :url, :body
7
+
8
+ attribute :author do |post|
9
+ post.author&.name
10
+ end
11
+
12
+ attribute :tags do |post|
13
+ post.tags.map(&:name).join(',')
14
+ end
15
+
16
+ attribute :featured_image do |post|
17
+ post&.featured_image&.url(:large)
18
+ end
19
+
20
+ attribute :preview_image do |post|
21
+ post&.preview_image&.url(:large)
22
+ end
23
+ end
24
+ end
@@ -2,7 +2,7 @@ module Integral
2
2
  # Handles uploading user avatars
3
3
  class AvatarUploader < ImageUploader
4
4
  # Provide a default URL as a default if there hasn't been a file uploaded
5
- def default_url
5
+ def default_url(*args)
6
6
  ActionController::Base.helpers.asset_path('integral/defaults/user_avatar.jpg')
7
7
  end
8
8
  end
@@ -0,0 +1,21 @@
1
+ .timeline-item
2
+ .timeline-icon
3
+ = icon(activity.item_icon)
4
+ .timeline-content
5
+ .timeline-content-header
6
+ %p.timeline-content-event= activity.event
7
+ - if activity.whodunnit.present?
8
+ = link_to activity.whodunnit_url, class: 'timeline-content-user' do
9
+ = image_tag activity.whodunnit_avatar_url, class: :avatar
10
+ %span= activity.whodunnit_name.truncate(30)
11
+ - else
12
+ %p.timeline-content-user
13
+ = image_tag activity.whodunnit_avatar_url, class: :avatar
14
+ %span= activity.whodunnit_name.truncate(30)
15
+ %p.timeline-content-date= l(activity.created_at)
16
+ %p.timeline-content-title
17
+ = activity.whodunnit_name
18
+ = activity.event_verb.downcase
19
+ = link_to activity.item_title, activity.item_url
20
+ %hr
21
+