integral 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (172) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +4 -3
  3. data/app/assets/javascripts/integral/backend.js +86 -2
  4. data/app/assets/javascripts/integral/support/character_counter.js +13 -8
  5. data/app/assets/javascripts/integral/support/list.coffee +1 -0
  6. data/app/assets/javascripts/integral/support/record_selector.coffee +2 -0
  7. data/app/assets/javascripts/integral/support/slug_generator.coffee +1 -0
  8. data/app/assets/stylesheets/integral/backend.sass +34 -23
  9. data/app/assets/stylesheets/integral/backend/_foundation_settings.scss +2 -2
  10. data/app/assets/stylesheets/integral/backend/dashboard-layout.scss +27 -63
  11. data/app/assets/stylesheets/integral/backend/devise.sass +2 -3
  12. data/app/assets/stylesheets/integral/backend/materialize-tags.sass +1 -0
  13. data/app/assets/stylesheets/integral/backend/modules/dropdown_pane_notifications.scss +150 -0
  14. data/app/assets/stylesheets/integral/backend/modules/dropdown_pane_profile.scss +59 -0
  15. data/app/assets/stylesheets/integral/backend/shared.sass +41 -2
  16. data/app/assets/stylesheets/integral/frontend/layout.sass +10 -0
  17. data/app/assets/stylesheets/integral/support/media-query-indicator.sass +4 -4
  18. data/app/controllers/integral/backend/activities_controller.rb +21 -27
  19. data/app/controllers/integral/backend/base_controller.rb +87 -37
  20. data/app/controllers/integral/backend/images_controller.rb +26 -8
  21. data/app/controllers/integral/backend/lists_controller.rb +2 -14
  22. data/app/controllers/integral/backend/notification_subscriptions_controller.rb +23 -0
  23. data/app/controllers/integral/backend/pages_controller.rb +0 -4
  24. data/app/controllers/integral/backend/posts_controller.rb +0 -4
  25. data/app/controllers/integral/backend/settings_controller.rb +4 -0
  26. data/app/controllers/integral/backend/static_pages_controller.rb +6 -0
  27. data/app/controllers/integral/backend/users_controller.rb +43 -24
  28. data/app/controllers/integral/blog_controller.rb +12 -0
  29. data/app/controllers/integral/categories_controller.rb +17 -3
  30. data/app/controllers/integral/tags_controller.rb +5 -2
  31. data/app/decorators/integral/base_decorator.rb +16 -0
  32. data/app/decorators/integral/image_decorator.rb +2 -2
  33. data/app/decorators/integral/list_decorator.rb +1 -13
  34. data/app/decorators/integral/notification/notification_decorator.rb +74 -0
  35. data/app/decorators/integral/page_decorator.rb +1 -13
  36. data/app/decorators/integral/post_decorator.rb +1 -2
  37. data/app/decorators/integral/user_decorator.rb +1 -13
  38. data/app/decorators/integral/version_decorator.rb +8 -4
  39. data/app/helpers/integral/backend/base_helper.rb +97 -31
  40. data/app/jobs/integral/application_job.rb +1 -0
  41. data/app/jobs/integral/newsletter_signup_job.rb +0 -2
  42. data/app/mailers/integral/devise_mailer.rb +6 -0
  43. data/app/models/concerns/integral/notification/subscribable.rb +67 -0
  44. data/app/models/integral/application_record.rb +9 -0
  45. data/app/models/integral/category.rb +9 -0
  46. data/app/models/integral/image.rb +40 -3
  47. data/app/models/integral/list.rb +10 -2
  48. data/app/models/integral/list_item.rb +14 -14
  49. data/app/models/integral/list_item_connection.rb +6 -0
  50. data/app/models/integral/notification/notification.rb +28 -0
  51. data/app/models/integral/notification/subscription.rb +14 -0
  52. data/app/models/integral/page.rb +15 -8
  53. data/app/models/integral/post.rb +11 -13
  54. data/app/models/integral/user.rb +45 -2
  55. data/app/policies/integral/base_policy.rb +7 -12
  56. data/app/policies/integral/page_policy.rb +1 -0
  57. data/app/policies/integral/version_policy.rb +0 -8
  58. data/app/views/devise/invitations/edit.haml +1 -4
  59. data/app/views/devise/mailer/invitation_instructions.inky-haml +20 -0
  60. data/app/views/integral/backend/activities/grid/_dropdown_actions.haml +1 -0
  61. data/app/views/integral/backend/activities/grid/_row_content.haml +13 -0
  62. data/app/views/integral/backend/activities/index.haml +7 -13
  63. data/app/views/integral/backend/activities/shared/_grid.haml +35 -20
  64. data/app/views/integral/backend/activities/shared/index.haml +12 -12
  65. data/app/views/integral/backend/activities/shared/show.haml +7 -7
  66. data/app/views/integral/backend/activities/show.haml +1 -1
  67. data/app/views/integral/backend/categories/_modal.haml +2 -3
  68. data/app/views/integral/backend/images/_form.haml +13 -25
  69. data/app/views/integral/backend/images/edit.haml +1 -9
  70. data/app/views/integral/backend/images/grid/_dropdown_actions.haml +5 -0
  71. data/app/views/integral/backend/images/grid/_row_content.haml +5 -0
  72. data/app/views/integral/backend/images/index.haml +11 -17
  73. data/app/views/integral/backend/images/list.haml +11 -0
  74. data/app/views/integral/backend/images/show.haml +26 -0
  75. data/app/views/integral/backend/lists/_form.haml +6 -19
  76. data/app/views/integral/backend/lists/_item_modal.haml +3 -3
  77. data/app/views/integral/backend/lists/_manager.haml +11 -13
  78. data/app/views/integral/backend/lists/edit.haml +6 -20
  79. data/app/views/integral/backend/lists/grid/_dropdown_actions.haml +9 -0
  80. data/app/views/integral/backend/lists/grid/_row_content.haml +3 -0
  81. data/app/views/integral/backend/lists/index.haml +11 -17
  82. data/app/views/integral/backend/lists/list.haml +11 -0
  83. data/app/views/integral/backend/lists/show.haml +30 -0
  84. data/app/views/integral/backend/notifications/_notification.haml +21 -0
  85. data/app/views/integral/backend/pages/_form.haml +19 -43
  86. data/app/views/integral/backend/pages/edit.haml +4 -12
  87. data/app/views/integral/backend/pages/grid/_dropdown_actions.haml +11 -0
  88. data/app/views/integral/backend/pages/grid/_row_content.haml +5 -0
  89. data/app/views/integral/backend/pages/index.haml +6 -6
  90. data/app/views/integral/backend/pages/list.haml +12 -19
  91. data/app/views/integral/backend/pages/show.haml +19 -35
  92. data/app/views/integral/backend/posts/_form.haml +18 -56
  93. data/app/views/integral/backend/posts/edit.haml +4 -14
  94. data/app/views/integral/backend/posts/grid/_dropdown_actions.haml +10 -0
  95. data/app/views/integral/backend/posts/grid/_row_content.haml +6 -0
  96. data/app/views/integral/backend/posts/index.haml +6 -6
  97. data/app/views/integral/backend/posts/list.haml +11 -18
  98. data/app/views/integral/backend/posts/new.haml +0 -1
  99. data/app/views/integral/backend/posts/show.haml +18 -41
  100. data/app/views/integral/backend/shared/_breadcrumbs.haml +7 -4
  101. data/app/views/integral/backend/shared/_image_preview.haml +10 -3
  102. data/app/views/integral/backend/shared/_image_selector.haml +1 -1
  103. data/app/views/integral/backend/shared/_notification_subscription_toggle.haml +22 -0
  104. data/app/views/integral/backend/shared/action_bar/_index.haml +9 -0
  105. data/app/views/integral/backend/shared/action_bar/_show.haml +3 -0
  106. data/app/views/integral/backend/shared/cards/_at_a_glance.haml +3 -3
  107. data/app/views/integral/backend/shared/cards/_categories.haml +27 -28
  108. data/app/views/integral/backend/shared/cards/_object.haml +1 -1
  109. data/app/views/integral/backend/shared/cards/_recent_activity.haml +12 -12
  110. data/app/views/integral/backend/shared/cards/_recent_resources.haml +17 -0
  111. data/app/views/integral/backend/shared/cards/_top_post_authors.haml +14 -15
  112. data/app/views/integral/backend/shared/cards/_welcome.haml +24 -25
  113. data/app/views/integral/backend/shared/{_empty_grid.haml → grid/_empty.haml} +0 -0
  114. data/app/views/integral/backend/shared/grid/_form.haml +9 -0
  115. data/app/views/integral/backend/shared/grid/_grid.haml +21 -0
  116. data/app/views/integral/backend/shared/{_pagination.haml → grid/_pagination.haml} +0 -0
  117. data/app/views/integral/backend/shared/grid/_row_layout.haml +8 -0
  118. data/app/views/integral/backend/shared/record_selector/_collection.haml +1 -0
  119. data/app/views/integral/backend/shared/record_selector/_modal.haml +9 -10
  120. data/app/views/integral/backend/static_pages/dashboard.haml +6 -7
  121. data/app/views/integral/backend/users/_form.haml +34 -46
  122. data/app/views/integral/backend/users/grid/_dropdown_actions.haml +17 -0
  123. data/app/views/integral/backend/users/grid/_row_content.haml +8 -0
  124. data/app/views/integral/backend/users/index.haml +6 -6
  125. data/app/views/integral/backend/users/list.haml +10 -16
  126. data/app/views/integral/backend/users/show.haml +10 -1
  127. data/app/views/integral/categories/show.haml +3 -3
  128. data/app/views/integral/posts/_article_footer.haml +1 -1
  129. data/app/views/integral/posts/_card.haml +1 -1
  130. data/app/views/integral/posts/_most_read_section.haml +1 -1
  131. data/app/views/integral/posts/_post.haml +1 -1
  132. data/app/views/integral/posts/templates/default.haml +1 -1
  133. data/app/views/integral/shared/sidebar/_item.haml +1 -1
  134. data/app/views/layouts/integral/backend.html.haml +24 -5
  135. data/app/views/layouts/integral/backend/_create_dropdown.haml +1 -30
  136. data/app/views/layouts/integral/backend/_main_menu_items.haml +1 -101
  137. data/config/initializers/devise.rb +1 -1
  138. data/config/locales/en.yml +60 -2
  139. data/config/routes.rb +2 -0
  140. data/db/migrate/20200407022636_create_integral_notifications.rb +25 -0
  141. data/db/migrate/20200421223602_add_status_to_integral_users.rb +5 -0
  142. data/db/seeds.rb +15 -11
  143. data/lib/integral.rb +1 -0
  144. data/lib/integral/acts_as_integral.rb +115 -0
  145. data/lib/integral/acts_as_listable.rb +1 -1
  146. data/lib/integral/engine.rb +9 -0
  147. data/lib/integral/grids/activities_grid.rb +0 -1
  148. data/lib/integral/grids/lists_grid.rb +1 -0
  149. data/lib/integral/grids/posts_grid.rb +5 -1
  150. data/lib/integral/grids/users_grid.rb +5 -0
  151. data/lib/integral/list_renderer.rb +5 -1
  152. data/lib/integral/router.rb +20 -4
  153. data/lib/integral/version.rb +1 -1
  154. data/spec/factories.rb +15 -1
  155. metadata +45 -39
  156. data/app/decorators/integral/category_version_decorator.rb +0 -7
  157. data/app/decorators/integral/image_version_decorator.rb +0 -7
  158. data/app/decorators/integral/list_version_decorator.rb +0 -7
  159. data/app/decorators/integral/page_version_decorator.rb +0 -7
  160. data/app/decorators/integral/post_version_decorator.rb +0 -7
  161. data/app/decorators/integral/user_version_decorator.rb +0 -7
  162. data/app/views/devise/mailer/invitation_instructions.html.erb +0 -13
  163. data/app/views/integral/backend/activities/_grid.haml +0 -22
  164. data/app/views/integral/backend/images/_grid.haml +0 -16
  165. data/app/views/integral/backend/lists/_grid.haml +0 -14
  166. data/app/views/integral/backend/pages/_grid.haml +0 -46
  167. data/app/views/integral/backend/posts/_grid.haml +0 -51
  168. data/app/views/integral/backend/shared/_grid.haml +0 -18
  169. data/app/views/integral/backend/shared/cards/_recent_pages.haml +0 -19
  170. data/app/views/integral/backend/shared/cards/_recent_posts.haml +0 -18
  171. data/app/views/integral/backend/shared/cards/_recent_users.haml +0 -19
  172. data/app/views/integral/backend/users/_grid.haml +0 -36
@@ -0,0 +1,6 @@
1
+ module Integral
2
+ class DeviseMailer < Devise::Mailer
3
+ layout 'integral/mailer'
4
+ helper 'integral/mail'
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ module Integral
2
+ module Notification
3
+ module Subscribable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_accessor :integral_notification_action
8
+
9
+ has_many :notification_subscriptions, as: :subscribable, class_name: "Integral::Notification::Subscription"
10
+
11
+ after_commit on: :create do
12
+ create_notifications(:create)
13
+ end
14
+
15
+ after_commit on: :update do
16
+ create_notifications(:update)
17
+ end
18
+
19
+ after_commit on: :destroy do
20
+ create_notifications(:destroy)
21
+ end
22
+ end
23
+
24
+ class_methods do
25
+ def notification_subscriptions(omit_users:)
26
+ Integral::Notification::Subscription.where(subscribable_type: self.name).where.not(user_id: omit_users)
27
+ end
28
+
29
+ def subscribable?
30
+ true
31
+ end
32
+ end
33
+
34
+ def subscribable?
35
+ true
36
+ end
37
+
38
+ def notifiable_users
39
+ object_notification_subscriptions = notification_subscriptions.group_by(&:state)
40
+ object_notification_subscription_user_ids = object_notification_subscriptions.values.flatten.map(&:user_id)
41
+
42
+ class_notification_subscriptions = self.class.notification_subscriptions(omit_users: object_notification_subscription_user_ids).group_by(&:state)
43
+ class_notification_subscription_user_ids = class_notification_subscriptions.values.flatten.map(&:user_id)
44
+
45
+ top_level_notification_subscription_user_ids = Integral::User.where(notify_me: true).where.not(id: object_notification_subscription_user_ids + class_notification_subscription_user_ids).pluck(:id)
46
+
47
+ notifiable_user_ids = (object_notification_subscriptions['subscribed']&.map(&:user_id) || []) + (class_notification_subscriptions['subscribed']&.map(&:user_id) || []) + top_level_notification_subscription_user_ids
48
+ notifiable_user_ids -= [PaperTrail.request.whodunnit]
49
+
50
+ User.find(notifiable_user_ids).select { |user| Pundit.policy!(user, self).receives_notifications? }
51
+ end
52
+
53
+ private
54
+
55
+ # TODO Improvements:
56
+ # - Move into a Job - Pass through the created_at time otherwise the notification sets it when the notifcation was created, rather than when the action was committed
57
+ # - Anyway to create multiple in one go but still run callbacks?
58
+ def create_notifications(action)
59
+ if PaperTrail.request.whodunnit
60
+ notifiable_users.each do |notifiable|
61
+ Integral::Notification::Notification.create!(subscribable: self, recipient: notifiable, action: (integral_notification_action || action), actor_id: PaperTrail.request.whodunnit)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -2,5 +2,14 @@ module Integral
2
2
  # Base Integral Model.
3
3
  class ApplicationRecord < ActiveRecord::Base
4
4
  self.abstract_class = true
5
+
6
+ # @return [Array] containing available human readable statuses against there numeric value
7
+ def self.available_statuses(opts = { reverse: false })
8
+ available_statuses = statuses.map do |key, value|
9
+ [I18n.t("integral.statuses.#{key}"), key]
10
+ end
11
+
12
+ opts[:reverse] ? available_statuses.each(&:reverse!) : available_statuses
13
+ end
5
14
  end
6
15
  end
@@ -1,6 +1,11 @@
1
1
  module Integral
2
2
  # Represents a user post category
3
3
  class Category < ApplicationRecord
4
+ acts_as_integral({
5
+ backend_main_menu: { enabled: false },
6
+ backend_create_menu: { enabled: false }
7
+ })
8
+
4
9
  has_paper_trail class_name: 'Integral::CategoryVersion'
5
10
 
6
11
  # Slugging
@@ -16,5 +21,9 @@ module Integral
16
21
  validates_format_of :slug, with: /\A[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*\z/
17
22
  validates :title, presence: true, length: { minimum: 4, maximum: 60 }
18
23
  validates :description, presence: true, length: { minimum: 25, maximum: 300 }
24
+
25
+ def self.integral_icon
26
+ 'tags'
27
+ end
19
28
  end
20
29
  end
@@ -3,8 +3,12 @@ module Integral
3
3
  class Image < ApplicationRecord
4
4
  before_save :touch_list_items
5
5
 
6
- # Soft-deletion
7
- acts_as_paranoid
6
+ acts_as_integral({
7
+ notifications: { enabled: false },
8
+ backend_main_menu: { order: 40 },
9
+ backend_create_menu: { order: 30 }
10
+ }) # Integral Goodness
11
+ acts_as_paranoid # Soft-deletion
8
12
 
9
13
  validates :file, presence: true
10
14
  validates :title, presence: true, length: { minimum: 5, maximum: 50 }
@@ -45,11 +49,44 @@ module Integral
45
49
  def self.listable_options
46
50
  {
47
51
  record_title: I18n.t('integral.backend.record_selector.images.record'),
48
- selector_path: Engine.routes.url_helpers.backend_images_path,
52
+ selector_path: Engine.routes.url_helpers.list_backend_images_path,
49
53
  selector_title: I18n.t('integral.backend.record_selector.images.title')
50
54
  }
51
55
  end
52
56
 
57
+ def self.integral_icon
58
+ 'image'
59
+ end
60
+
61
+ def self.integral_backend_main_menu_item
62
+ {
63
+ icon: integral_icon,
64
+ order: integral_options.dig(:backend_main_menu, :order),
65
+ label: model_name.human.pluralize,
66
+ url: url_helpers.send("backend_img_index_url"),
67
+ authorize_class: self,
68
+ authorize_action: :index,
69
+ list_items: [
70
+ { label: I18n.t('integral.navigation.dashboard'), url: url_helpers.send("backend_img_index_url"), authorize_class: self, authorize_action: :index },
71
+ { label: I18n.t('integral.actions.create'), url: url_helpers.send("new_backend_img_url"), authorize_class: self, authorize_action: :new },
72
+ { label: I18n.t('integral.navigation.listing'), url: url_helpers.send("list_backend_img_index_url"), authorize_class: self, authorize_action: :list },
73
+ ]
74
+ }
75
+ end
76
+
77
+ # @return [Hash] hash representing the class, used to render within the create menu
78
+ def self.integral_backend_create_menu_item
79
+ {
80
+ icon: integral_icon,
81
+ order: integral_options.dig(:backend_create_menu, :order),
82
+ label: model_name.human,
83
+ url: url_helpers.send("new_backend_img_url"),
84
+ # authorize: proc { policy(self).index? }, can't use this as self is in wrong context
85
+ authorize_class: self,
86
+ authorize_action: :new,
87
+ }
88
+ end
89
+
53
90
  private
54
91
 
55
92
  def touch_list_items
@@ -3,8 +3,12 @@ module Integral
3
3
  class List < ApplicationRecord
4
4
  default_scope { includes(:list_items) }
5
5
 
6
- # Soft-deletion
7
- acts_as_paranoid
6
+ acts_as_integral({
7
+ backend_main_menu: { order: 50 },
8
+ backend_create_menu: { order: 40 }
9
+ }) # Integral Goodness
10
+
11
+ acts_as_paranoid # Soft-deletion
8
12
 
9
13
  # Associations
10
14
  has_many :list_items
@@ -48,6 +52,10 @@ module Integral
48
52
  new_list
49
53
  end
50
54
 
55
+ def self.integral_icon
56
+ 'list'
57
+ end
58
+
51
59
  private
52
60
 
53
61
  def validate_unlocked
@@ -1,24 +1,17 @@
1
1
  module Integral
2
2
  # Represents an item within a particular list
3
3
  class ListItem < ApplicationRecord
4
- after_initialize :set_defaults
5
- after_touch :touch_list
6
-
7
4
  # Default scope orders by priority and includes children
8
5
  default_scope { includes(:children).includes(:image).order(:priority) }
9
6
 
10
7
  # Associations
11
8
  belongs_to :list, optional: true, touch: true
12
9
  belongs_to :image, optional: true
13
- has_and_belongs_to_many(:children,
14
- -> { order(:priority) },
15
- join_table: 'integral_list_item_connections',
16
- foreign_key: 'parent_id',
17
- association_foreign_key: 'child_id',
18
- class_name: 'ListItem',
19
- after_add: :touch_updated_at,
20
- after_remove: :touch_updated_at,
21
- optional: true)
10
+ has_many :list_item_connections, foreign_key: 'parent_id'
11
+ has_many :children, -> { order(:priority) }, through: :list_item_connections
12
+ has_many :inverse_list_item_connections, class_name: "ListItemConnection", foreign_key: "child_id"
13
+ # NOTE: A List Item only has one parent
14
+ has_many :parents, :through => :inverse_list_item_connections
22
15
 
23
16
  # Validations
24
17
  validate :validate_child_absence
@@ -26,6 +19,12 @@ module Integral
26
19
  # Nested forms
27
20
  accepts_nested_attributes_for :children, reject_if: :all_blank, allow_destroy: true
28
21
 
22
+ # Callbacks
23
+ after_initialize :set_defaults
24
+ after_commit :touch_parent, on: :update
25
+ after_destroy :touch_parent
26
+ after_touch :touch_list
27
+
29
28
  # @return [Array] list of types available for a list item
30
29
  def self.types_collection
31
30
  collection = [
@@ -69,8 +68,9 @@ module Integral
69
68
  self.type ||= 'Integral::Basic'
70
69
  end
71
70
 
72
- def touch_updated_at(_list_item)
73
- touch if persisted?
71
+ def touch_parent
72
+ parent = parents.first
73
+ parent.touch if parent.present? && parent.persisted?
74
74
  end
75
75
 
76
76
  def touch_list
@@ -0,0 +1,6 @@
1
+ module Integral
2
+ class ListItemConnection < ApplicationRecord
3
+ belongs_to :parent, touch: true, class_name: 'Integral::ListItem'
4
+ belongs_to :child, touch: true, class_name: 'Integral::ListItem', dependent: :destroy
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ module Integral
2
+ module Notification
3
+ class Notification < ApplicationRecord
4
+ belongs_to :recipient, class_name: "User"
5
+ belongs_to :actor, class_name: "User", optional: true
6
+ belongs_to :subscribable, polymorphic: true
7
+
8
+ scope :unread, ->{ where(read_at: nil) }
9
+ scope :recent, ->{ order(created_at: :desc).limit(per_page) }
10
+
11
+ self.per_page = 8
12
+
13
+ validates :action, presence: true
14
+
15
+ def read!
16
+ update!(read_at: Time.now())
17
+ end
18
+
19
+ def unread?
20
+ read_at.nil?
21
+ end
22
+
23
+ def to_partial_path
24
+ 'integral/backend/notifications/notification'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ module Integral
2
+ module Notification
3
+ class Subscription < ApplicationRecord
4
+ self.table_name = 'integral_notification_subscriptions'
5
+
6
+ belongs_to :subscribable, polymorphic: true, optional: true
7
+ belongs_to :user
8
+
9
+ enum state: { subscribed: 'subscribed', unsubscribed: 'unsubscribed' }
10
+
11
+ validates :subscribable_type, :state, presence: true
12
+ end
13
+ end
14
+ end
@@ -4,6 +4,11 @@ module Integral
4
4
  include LazyContentable
5
5
 
6
6
  acts_as_paranoid # Soft-deletion
7
+ acts_as_integral({
8
+ backend_main_menu: { order: 20 },
9
+ backend_create_menu: { order: 10 }
10
+ }) # Integral Goodness
11
+
7
12
  acts_as_listable # Listable Item
8
13
 
9
14
  has_paper_trail class_name: 'Integral::PageVersion'
@@ -35,6 +40,7 @@ module Integral
35
40
 
36
41
  # Callbacks
37
42
  before_save :set_paper_trail_event
43
+ before_save :set_integral_notification_action
38
44
 
39
45
  # Scopes
40
46
  scope :search, ->(query) { where('lower(title) LIKE ? OR lower(path) LIKE ?', "%#{query.downcase}%", "%#{query.downcase}%") }
@@ -71,11 +77,15 @@ module Integral
71
77
  {
72
78
  icon: 'file',
73
79
  record_title: I18n.t('integral.backend.record_selector.pages.record'),
74
- selector_path: Engine.routes.url_helpers.backend_pages_path,
80
+ selector_path: Engine.routes.url_helpers.list_backend_pages_path,
75
81
  selector_title: I18n.t('integral.backend.record_selector.pages.title')
76
82
  }
77
83
  end
78
84
 
85
+ def self.integral_icon
86
+ 'file'
87
+ end
88
+
79
89
  # @return [Hash] the instance as a card
80
90
  def to_card
81
91
  # subtitle = self.published_at.present? ? I18n.t('integral.blog.posted_ago', time: time_ago_in_words(self.published_at)) : I18n.t('integral.records.status.draft')
@@ -136,13 +146,10 @@ module Integral
136
146
  end
137
147
  end
138
148
 
139
- # @return [Array] containing available human readable statuses against there numeric value
140
- def self.available_statuses
141
- [
142
- ['Draft', 0],
143
- ['Published', 1],
144
- ['Archived', 2]
145
- ]
149
+ def set_integral_notification_action
150
+ if persisted? && published? && status_changed?
151
+ self.integral_notification_action = :publish
152
+ end
146
153
  end
147
154
 
148
155
  def validate_parent_is_available
@@ -5,6 +5,10 @@ module Integral
5
5
  include LazyContentable
6
6
  include Webhook::Observable
7
7
 
8
+ acts_as_integral({
9
+ backend_main_menu: { order: 30, enabled: Integral.blog_enabled? },
10
+ backend_create_menu: { order: 20, enabled: Integral.blog_enabled? }
11
+ }) # Integral Goodness
8
12
  acts_as_paranoid # Soft-deletion
9
13
  acts_as_listable if Integral.blog_enabled? # Listable Item
10
14
  acts_as_taggable # Tagging
@@ -21,7 +25,7 @@ module Integral
21
25
  self.per_page = 8 if respond_to? :per_page
22
26
 
23
27
  # Associations
24
- belongs_to :user
28
+ belongs_to :user, -> { with_deleted }
25
29
  belongs_to :category
26
30
  belongs_to :image, class_name: 'Integral::Image', optional: true
27
31
  belongs_to :preview_image, class_name: 'Integral::Image', optional: true
@@ -45,16 +49,6 @@ module Integral
45
49
  # Scopes
46
50
  scope :search, ->(query) { where('lower(title) LIKE ? OR lower(slug) LIKE ?', "%#{query.downcase}%", "%#{query.downcase}%") }
47
51
 
48
- # @return [Array] containing available human readable statuses against there numeric value
49
- def self.available_statuses(opts = { reverse: false })
50
- statuses = [
51
- [I18n.t('integral.records.status.draft'), 0],
52
- [I18n.t('integral.records.status.published'), 1]
53
- ]
54
-
55
- opts[:reverse] ? statuses.each(&:reverse!) : statuses
56
- end
57
-
58
52
  # Increments the view count of the post if a PostViewing is successfully added
59
53
  #
60
54
  # @param ip_address [String] Viewers IP address
@@ -101,11 +95,15 @@ module Integral
101
95
  {
102
96
  icon: 'rss',
103
97
  record_title: I18n.t('integral.backend.record_selector.posts.record'),
104
- selector_path: Engine.routes.url_helpers.backend_posts_path,
98
+ selector_path: Engine.routes.url_helpers.list_backend_posts_path,
105
99
  selector_title: I18n.t('integral.backend.record_selector.posts.title')
106
100
  }
107
101
  end
108
102
 
103
+ def self.integral_icon
104
+ 'rss'
105
+ end
106
+
109
107
  # @return [String] Current tag context
110
108
  def tag_context
111
109
  status
@@ -166,7 +164,7 @@ module Integral
166
164
  end
167
165
 
168
166
  def deliver_published_webhook_on_update
169
- deliver_webhook(:published) if status_changed? && published?
167
+ deliver_webhook(:published) if saved_change_to_status? && published?
170
168
  end
171
169
 
172
170
  def deliver_published_webhook_on_create
@@ -1,8 +1,11 @@
1
1
  module Integral
2
2
  # User model used to represent a authenticated user
3
3
  class User < ApplicationRecord
4
- # Soft-deletion
5
- acts_as_paranoid
4
+ acts_as_paranoid # Soft-deletion
5
+ acts_as_integral({
6
+ backend_main_menu: { order: 60 },
7
+ backend_create_menu: { order: 50 }
8
+ }) # Integral Goodness
6
9
 
7
10
  mount_uploader :avatar, AvatarUploader
8
11
  process_in_background :avatar
@@ -11,9 +14,14 @@ module Integral
11
14
  # :confirmable, :timeoutable, :omniauthable, registerable and lockable
12
15
  devise :invitable, :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable
13
16
 
17
+ enum status: %i[pending active blocked]
18
+
14
19
  # Relations
15
20
  has_many :role_assignments
16
21
  has_many :roles, through: :role_assignments
22
+ # notification_subscription is used by Subscribable concern - Users can have notification_subscriptions AND be subscribable
23
+ has_many :own_notification_subscriptions, class_name: "Integral::Notification::Subscription"
24
+ has_many :notifications, class_name: "Integral::Notification::Notification", foreign_key: :recipient_id
17
25
 
18
26
  # Validations
19
27
  validates :name, :email, presence: true
@@ -34,6 +42,10 @@ module Integral
34
42
  roles.map { |r| r.name.underscore.to_sym }.any? { |user_role| role_sym.include?(user_role) }
35
43
  end
36
44
 
45
+ def self.integral_icon
46
+ 'user'
47
+ end
48
+
37
49
  # @return [Array] containing available locales
38
50
  def self.available_locales
39
51
  available_locales = []
@@ -56,6 +68,37 @@ module Integral
56
68
  super
57
69
  end
58
70
 
71
+ def multiple_page_notifications?
72
+ notifications.count > Integral::Notification::Notification.per_page
73
+ end
74
+
75
+ # @param subscribable [Class or Instance]
76
+ def receives_notifications_for?(subscribable)
77
+ if subscribable.is_a?(Class)
78
+ subscription = own_notification_subscriptions.find_by(subscribable_type: subscribable.name, subscribable_id: nil)
79
+
80
+ return subscription.subscribed? if subscription
81
+ else
82
+ instance_subscription = own_notification_subscriptions.find_by(subscribable_type: subscribable.class.name, subscribable_id: subscribable.id)
83
+
84
+ return instance_subscription.subscribed? if instance_subscription
85
+
86
+ class_level_subscription = own_notification_subscriptions.find_by(subscribable_type: subscribable.class.name, subscribable_id: nil)
87
+
88
+ return class_level_subscription.subscribed? if class_level_subscription
89
+ end
90
+
91
+ notify_me
92
+ end
93
+
94
+ def active_for_authentication?
95
+ super && !blocked?
96
+ end
97
+
98
+ def inactive_message
99
+ blocked? ? :blocked : super
100
+ end
101
+
59
102
  private
60
103
 
61
104
  def send_devise_notification(notification, *args)