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.
- checksums.yaml +4 -4
- data/README.md +2 -30
- data/Rakefile +1 -1
- data/app/assets/images/integral/defaults/no_image_available.jpg +0 -0
- data/app/assets/javascripts/integral/backend.js +102 -11
- data/app/assets/javascripts/integral/frontend.js +37 -0
- data/app/assets/javascripts/integral/support/confirm_modal.coffee +2 -2
- data/app/assets/javascripts/integral/support/gallery.coffee +71 -54
- data/app/assets/javascripts/integral/support/lib/lazysizes.js +755 -0
- data/app/assets/javascripts/integral/support/lib/materialize-tags.js +49 -44
- data/app/assets/javascripts/integral/support/ls.instagram.js +57 -0
- data/app/assets/javascripts/integral/support/ls.twitter.js +66 -0
- data/app/assets/javascripts/integral/support/record_selector.coffee +1 -1
- data/app/assets/javascripts/integral/support/remote_form.coffee +5 -2
- data/app/assets/stylesheets/integral/backend.sass +2 -1
- data/app/assets/stylesheets/integral/backend/_foundation_settings.scss +3 -4
- data/app/assets/stylesheets/integral/backend/dashboard-layout.scss +4 -1
- data/app/assets/stylesheets/integral/backend/materialize-tags.sass +1 -1
- data/app/assets/stylesheets/integral/backend/modules/timeline.scss +214 -0
- data/app/assets/stylesheets/integral/backend/shared.sass +80 -11
- data/app/assets/stylesheets/integral/frontend.scss +45 -0
- data/app/assets/stylesheets/integral/frontend/_foundation_settings.scss +2 -2
- data/app/assets/stylesheets/integral/frontend/blog.sass +155 -142
- data/app/assets/stylesheets/integral/frontend/layout.sass +3 -3
- data/app/assets/stylesheets/integral/frontend/modules/article-footer.scss +55 -0
- data/app/assets/stylesheets/integral/frontend/modules/article.scss +34 -0
- data/app/assets/stylesheets/integral/frontend/modules/horizontal-post.scss +44 -0
- data/app/assets/stylesheets/integral/frontend/modules/inline-articles.scss +23 -0
- data/app/assets/stylesheets/integral/frontend/modules/latest-post.scss +37 -0
- data/app/assets/stylesheets/integral/frontend/modules/list-widget.scss +50 -0
- data/app/assets/stylesheets/integral/frontend/modules/piped-list.scss +33 -0
- data/app/assets/stylesheets/integral/frontend/modules/post-tags.scss +19 -0
- data/app/assets/stylesheets/integral/frontend/modules/scroll-container.scss +9 -0
- data/app/assets/stylesheets/integral/frontend/modules/sidebar-articles.scss +42 -0
- data/app/assets/stylesheets/integral/frontend/modules/sidebar-tags.scss +6 -0
- data/app/assets/stylesheets/integral/frontend/modules/sidebar-widget.scss +47 -0
- data/app/assets/stylesheets/integral/frontend/modules/vertical-post.scss +31 -0
- data/app/assets/stylesheets/integral/frontend/share_modal.sass +0 -5
- data/app/assets/stylesheets/integral/support/gallery.sass +8 -0
- data/app/assets/stylesheets/integral/support/media-query-indicator.sass +6 -0
- data/app/controllers/integral/application_controller.rb +7 -1
- data/app/controllers/integral/backend/activities_controller.rb +13 -2
- data/app/controllers/integral/backend/base_controller.rb +60 -7
- data/app/controllers/integral/backend/categories_controller.rb +49 -0
- data/app/controllers/integral/backend/pages_controller.rb +7 -2
- data/app/controllers/integral/backend/posts_controller.rb +8 -3
- data/app/controllers/integral/backend/static_pages_controller.rb +4 -0
- data/app/controllers/integral/backend/users_controller.rb +13 -7
- data/app/controllers/integral/categories_controller.rb +31 -0
- data/app/controllers/integral/pages_controller.rb +1 -1
- data/app/controllers/integral/posts_controller.rb +5 -3
- data/app/decorators/integral/category_decorator.rb +30 -0
- data/app/decorators/integral/category_version_decorator.rb +7 -0
- data/app/decorators/integral/image_version_decorator.rb +7 -0
- data/app/decorators/integral/list_decorator.rb +1 -1
- data/app/decorators/integral/list_version_decorator.rb +7 -0
- data/app/decorators/integral/page_version_decorator.rb +7 -0
- data/app/decorators/integral/post_decorator.rb +9 -1
- data/app/decorators/integral/post_version_decorator.rb +7 -0
- data/app/decorators/integral/user_decorator.rb +1 -1
- data/app/decorators/integral/user_version_decorator.rb +7 -0
- data/app/decorators/integral/version_decorator.rb +51 -12
- data/app/helpers/integral/backend/base_helper.rb +56 -2
- data/app/helpers/integral/blog_helper.rb +21 -4
- data/app/jobs/integral/webhook/delivery_job.rb +37 -0
- data/app/mailers/integral/contact_mailer.rb +4 -1
- data/app/models/concerns/integral/lazy_contentable.rb +54 -0
- data/app/models/concerns/integral/webhook/delivery.rb +30 -0
- data/app/models/concerns/integral/webhook/observable.rb +23 -0
- data/app/models/integral/category.rb +20 -0
- data/app/models/integral/category_version.rb +8 -0
- data/app/models/integral/list_item.rb +1 -2
- data/app/models/integral/page.rb +18 -3
- data/app/models/integral/post.rb +28 -1
- data/app/models/integral/version.rb +2 -2
- data/app/models/integral/webhook/endpoint.rb +40 -0
- data/app/models/integral/webhook/event.rb +20 -0
- data/app/policies/integral/base_policy.rb +1 -0
- data/app/policies/integral/category_policy.rb +9 -0
- data/app/serializers/integral/post_serializer.rb +24 -0
- data/app/uploaders/integral/avatar_uploader.rb +1 -1
- data/app/views/integral/backend/activities/_activity.haml +21 -0
- data/app/views/integral/backend/activities/_grid.haml +1 -2
- data/app/views/integral/backend/activities/shared/_grid.haml +3 -2
- data/app/views/integral/backend/activities/shared/{_listing.haml → index.haml} +1 -0
- data/app/views/integral/backend/activities/shared/{_log.haml → show.haml} +0 -0
- data/app/views/integral/backend/categories/_modal.haml +25 -0
- data/app/views/integral/backend/lists/_child_fields.haml +1 -1
- data/app/views/integral/backend/lists/_item_container.haml +1 -1
- data/app/views/integral/backend/lists/_item_modal.haml +1 -1
- data/app/views/integral/backend/lists/_list_item_fields.haml +1 -1
- data/app/views/integral/backend/pages/_form.haml +1 -4
- data/app/views/integral/backend/pages/_grid.haml +34 -9
- data/app/views/integral/backend/pages/edit.haml +9 -3
- data/app/views/integral/backend/pages/index.haml +11 -21
- data/app/views/integral/backend/pages/list.haml +22 -0
- data/app/views/integral/backend/pages/show.haml +48 -0
- data/app/views/integral/backend/posts/_form.haml +8 -6
- data/app/views/integral/backend/posts/_grid.haml +33 -7
- data/app/views/integral/backend/posts/index.haml +13 -19
- data/app/views/integral/backend/posts/list.haml +20 -0
- data/app/views/integral/backend/posts/show.haml +54 -0
- data/app/views/integral/backend/shared/_activity_modal.haml +13 -0
- data/app/views/integral/backend/shared/cards/_categories.haml +34 -0
- data/app/views/integral/backend/{static_pages/_card.haml → shared/cards/_object.haml} +0 -0
- data/app/views/integral/backend/shared/cards/_recent_activity.haml +20 -0
- data/app/views/integral/backend/shared/cards/_recent_pages.haml +19 -0
- data/app/views/integral/backend/shared/cards/_recent_posts.haml +18 -0
- data/app/views/integral/backend/shared/cards/_recent_user_activity.haml +1 -0
- data/app/views/integral/backend/shared/cards/_recent_users.haml +19 -0
- data/app/views/integral/backend/shared/cards/_top_post_authors.haml +19 -0
- data/app/views/integral/backend/shared/record_selector/_record.haml +6 -4
- data/app/views/integral/backend/static_pages/dashboard.haml +13 -11
- data/app/views/integral/backend/users/_grid.haml +24 -7
- data/app/views/integral/backend/users/index.haml +11 -17
- data/app/views/integral/backend/users/list.haml +18 -0
- data/app/views/integral/backend/users/show.haml +5 -11
- data/app/views/integral/categories/show.haml +5 -0
- data/app/views/integral/posts/_article_footer.haml +17 -0
- data/app/views/integral/posts/_card.haml +11 -0
- data/app/views/integral/posts/_latest_post.haml +8 -0
- data/app/views/integral/posts/_most_read_section.haml +8 -0
- data/app/views/integral/posts/_post.haml +11 -0
- data/app/views/integral/posts/_similar_posts.haml +5 -0
- data/app/views/integral/posts/index.haml +6 -5
- data/app/views/integral/posts/templates/default.haml +34 -33
- data/app/views/integral/shared/_subscribe_modal.haml +14 -0
- data/app/views/integral/shared/blog/_categories.haml +15 -0
- data/app/views/integral/shared/blog/_layout.haml +9 -0
- data/app/views/integral/shared/blog/_sidebar.haml +10 -0
- data/app/views/integral/shared/gallery/_placeholder.haml +1 -1
- data/app/views/integral/shared/gallery/_slide.haml +2 -2
- data/app/views/integral/shared/gallery/gallery.haml +5 -2
- data/app/views/integral/shared/sidebar/_item.haml +8 -0
- data/app/views/integral/shared/sidebar/_newsletter_signup.haml +7 -0
- data/app/views/integral/shared/sidebar/_popular_posts.haml +7 -0
- data/app/views/integral/shared/sidebar/_popular_tags.haml +7 -0
- data/app/views/integral/shared/sidebar/_recent_posts.haml +7 -0
- data/app/views/integral/tags/index.haml +2 -2
- data/app/views/integral/tags/show.haml +3 -6
- data/app/views/layouts/integral/backend.html.haml +3 -0
- data/app/views/layouts/integral/backend/_main_menu_items.haml +10 -0
- data/app/views/layouts/integral/frontend.html.haml +3 -3
- data/config/locales/en.yml +52 -49
- data/db/migrate/20190414172018_create_webhook_endpoints.rb +10 -0
- data/db/migrate/20190929191412_add_integral_post_categories.rb +13 -0
- data/db/migrate/20191203090008_add_image_to_integral_categories.rb +6 -0
- data/db/migrate/20200401210442_create_category_versions.rb +20 -0
- data/db/seeds.rb +3 -1
- data/lib/generators/integral/assets_generator.rb +2 -2
- data/lib/generators/integral/install_generator.rb +1 -1
- data/lib/generators/integral/views_generator.rb +1 -1
- data/lib/generators/templates/integral.rb +5 -0
- data/lib/integral.rb +3 -30
- data/lib/integral/acts_as_listable.rb +2 -2
- data/lib/integral/chart_renderer/base.rb +2 -0
- data/lib/integral/content_renderer.rb +2 -2
- data/lib/integral/engine.rb +2 -2
- data/lib/integral/grids/activities_grid.rb +15 -1
- data/lib/integral/list_item_renderer.rb +4 -2
- data/lib/integral/list_renderer.rb +1 -0
- data/lib/integral/middleware/page_router.rb +15 -6
- data/lib/integral/router.rb +19 -3
- data/lib/integral/version.rb +1 -1
- data/lib/integral/widgets/swiper_list.rb +3 -2
- data/public/images/integral/demo/continous-integration.png +0 -0
- data/public/images/integral/demo/foundation-frontend-framework.jpg +0 -0
- data/public/images/integral/demo/heroku.png +0 -0
- data/public/images/integral/demo/integral-cms-without-hassle.jpg +0 -0
- data/public/images/integral/demo/integral-features-activity-tracking.jpg +0 -0
- data/public/images/integral/demo/integral-features-contact-form.png +0 -0
- data/public/images/integral/demo/integral-features-design.jpg +0 -0
- data/public/images/integral/demo/integral-features-dynamic-pages.jpg +0 -0
- data/public/images/integral/demo/integral-features-image-management.jpg +0 -0
- data/public/images/integral/demo/integral-features-integrated-blog.jpg +0 -0
- data/public/images/integral/demo/integral-features-list-management.jpg +0 -0
- data/public/images/integral/demo/integral-features-seo-ready.jpg +0 -0
- data/public/images/integral/demo/integral-features-user-management.jpg +0 -0
- data/public/images/integral/demo/integral-presentation.png +0 -0
- data/spec/factories.rb +15 -7
- metadata +110 -98
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/copywidget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/editwidget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/copywidget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/editwidget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/removewidget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/hidpi/widget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/removewidget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/icons/widget.png +0 -0
- data/app/assets/javascripts/ckeditor/plugins/integral-card/plugin.js +0 -86
- data/app/assets/javascripts/ckeditor/plugins/integralrecentposts/dialogs/integralrecentposts.js +0 -40
- data/app/assets/javascripts/ckeditor/plugins/integralrecentposts/plugin.js +0 -32
- data/app/assets/javascripts/ckeditor/plugins/numericinput/LICENSE.md +0 -363
- data/app/assets/javascripts/ckeditor/plugins/numericinput/README.md +0 -16
- data/app/assets/javascripts/ckeditor/plugins/numericinput/plugin.js +0 -354
- data/app/assets/stylesheets/integral/frontend.sass +0 -25
- data/app/views/integral/backend/pages/activities.haml +0 -2
- data/app/views/integral/backend/pages/activity.haml +0 -1
- data/app/views/integral/backend/posts/activities.haml +0 -3
- data/app/views/integral/backend/posts/activity.haml +0 -1
- data/app/views/integral/posts/_collection.haml +0 -4
- data/app/views/integral/posts/_item.haml +0 -16
- data/app/views/integral/shared/_blog_layout.haml +0 -15
- data/app/views/integral/shared/_blog_sidebar.haml +0 -49
- 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
|
|
@@ -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) },
|
data/app/models/integral/page.rb
CHANGED
|
@@ -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 =
|
|
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
|
data/app/models/integral/post.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
+
|