Class: Collavre::Creative

Inherits:
ApplicationRecord show all
Includes:
Describable, Linkable, Permissible, RealtimeBroadcastable
Defined in:
app/models/collavre/creative.rb,
app/models/collavre/creative/linkable.rb,
app/models/collavre/creative/describable.rb,
app/models/collavre/creative/permissible.rb,
app/models/collavre/creative/realtime_broadcastable.rb

Defined Under Namespace

Modules: Describable, Linkable, Permissible, RealtimeBroadcastable

Constant Summary collapse

SYSTEM_TOPIC_NAME =
"System"
MAIN_TOPIC_NAME =
"Main"
CONTENT_TOPIC_NAME =
"Content"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from RealtimeBroadcastable

#add_progress_text!, broadcast_batch_created, #broadcast_creative_created, #broadcast_creative_destroyed, #broadcast_creative_updated, broadcast_excludable_user_id, #broadcast_excludable_user_id, #broadcast_node_payload, #build_ancestor_ids_from_parent, #build_linked_creative_map, #capture_broadcast_state, #enqueue_broadcast, #find_broadcast_users, #format_progress_text, #previous_sibling, #progress_only_change?, #safe_effective_origin

Methods included from Describable

#creative_snippet, #effective_description

Methods included from Permissible

#all_shared_users, #children_with_permission, #find_ai_agent, #has_permission?

Methods included from Linkable

#create_linked_creative_for_user, #effective_attribute, #effective_origin, #linked_children, #progress, #user

Instance Attribute Details

#filtered_progressObject

Returns the value of attribute filtered_progress.



84
85
86
# File 'app/models/collavre/creative.rb', line 84

def filtered_progress
  @filtered_progress
end

#skip_github_validationObject

Returns the value of attribute skip_github_validation.



44
45
46
# File 'app/models/collavre/creative.rb', line 44

def skip_github_validation
  @skip_github_validation
end

Class Method Details

.inbox_for(user) ⇒ Object

Find or create the inbox creative for a given user. Places it as a root creative (no parent) owned by the user.



72
73
74
75
76
77
78
79
80
81
82
# File 'app/models/collavre/creative.rb', line 72

def self.inbox_for(user)
  existing = where(user: user).inboxes.first
  return existing if existing

  create!(
    description: I18n.t("collavre.inbox.default_name"),
    data: { "kind" => "inbox" },
    user: user,
    progress: 0.0
  )
end

.shared_accessible_ids(user) ⇒ Object

Returns IDs of creatives that have at least one active share and are accessible by the given user (owned or shared with/by them). Includes linked creatives whose origin has shares.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'app/models/collavre/creative.rb', line 101

def self.shared_accessible_ids(user)
  own_ids = where(user_id: user.id).pluck(:id)
  shared_ids = CreativeShare
    .where.not(permission: :no_access)
    .where("user_id = :uid OR shared_by_id = :uid", uid: user.id)
    .pluck(:creative_id)

  accessible_ids = (own_ids | shared_ids).uniq

  # Direct shares on accessible creatives
  directly_shared = CreativeShare
    .where(creative_id: accessible_ids)
    .where.not(permission: :no_access)
    .pluck(:creative_id)

  # Linked creatives whose origin has shares
  origin_shared = where(id: accessible_ids)
    .where.not(origin_id: nil)
    .joins("INNER JOIN creative_shares ON creative_shares.creative_id = creatives.origin_id")
    .where.not(creative_shares: { permission: :no_access })
    .pluck(:id)

  (directly_shared | origin_shared).uniq
end

Instance Method Details

#archive!Object



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/collavre/creative.rb', line 224

def archive!
  now = Time.current
  self.class.transaction do
    # Archive self and descendants
    self_and_descendants.where(archived_at: nil).update_all(archived_at: now)

    # If this is a linked creative, also archive the origin and its descendants
    origin = effective_origin(Set.new)
    if origin != self
      origin.self_and_descendants.where(archived_at: nil).update_all(archived_at: now)
      # Archive all other linked creatives of the origin
      origin.linked_creatives.where.not(id: id).find_each do |linked|
        linked.self_and_descendants.where(archived_at: nil).update_all(archived_at: now)
      end
    end

    # Also archive any linked creatives that point to this one
    linked_creatives.find_each do |linked|
      linked.self_and_descendants.where(archived_at: nil).update_all(archived_at: now)
    end

    reload
    parent&.reload
    Collavre::Creatives::ProgressService.new(parent).update_progress_from_children! if parent
  end
end

#archived?Boolean

— Archive —

Returns:

  • (Boolean)


220
221
222
# File 'app/models/collavre/creative.rb', line 220

def archived?
  archived_at.present?
end

#childrenObject



185
186
187
188
# File 'app/models/collavre/creative.rb', line 185

def children
  # better not override this method, use children_with_permission instead or linked_children
  super
end

#content_topic(fallback_user: user) ⇒ Object



64
65
66
67
68
# File 'app/models/collavre/creative.rb', line 64

def content_topic(fallback_user: user)
  topics.find_or_create_by!(name: CONTENT_TOPIC_NAME) do |topic|
    topic.user = fallback_user
  end
end

#context_creativesObject

Returns context creatives (excludes self to avoid duplication).



173
174
175
176
177
# File 'app/models/collavre/creative.rb', line 173

def context_creatives
  ids = effective_context_ids
  ids -= [ id ]
  Creative.where(id: ids)
end

#context_idsObject

— Context IDs — Returns the directly-configured context creative IDs for this creative.



143
144
145
# File 'app/models/collavre/creative.rb', line 143

def context_ids
  Array(data&.dig("context_ids"))
end

#disabled_context_idsObject

Returns the directly-configured disabled context IDs for this creative.



148
149
150
# File 'app/models/collavre/creative.rb', line 148

def disabled_context_ids
  Array(data&.dig("disabled_context_ids"))
end

#drop_trigger_enabled?Boolean

— Drop Trigger —

Returns:

  • (Boolean)


137
138
139
# File 'app/models/collavre/creative.rb', line 137

def drop_trigger_enabled?
  data&.dig("trigger", "on_child_enter") == true
end

#effective_context_ids(visited_ids = Set.new) ⇒ Object

Returns the effective context IDs: own + inherited from ancestors (deduplicated).



153
154
155
156
157
158
159
160
# File 'app/models/collavre/creative.rb', line 153

def effective_context_ids(visited_ids = Set.new)
  return [] if visited_ids.include?(id)

  visited_ids.add(id)
  own = context_ids
  parent_ctx = parent&.effective_context_ids(visited_ids) || []
  (own + parent_ctx).uniq
end

#effective_disabled_context_ids(visited_ids = Set.new) ⇒ Object

Returns the effective disabled context IDs: own + inherited from ancestors (deduplicated).



163
164
165
166
167
168
169
170
# File 'app/models/collavre/creative.rb', line 163

def effective_disabled_context_ids(visited_ids = Set.new)
  return [] if visited_ids.include?(id)

  visited_ids.add(id)
  own = disabled_context_ids
  parent_disabled = parent&.effective_disabled_context_ids(visited_ids) || []
  (own + parent_disabled).uniq
end

#github_markdown?Boolean

Returns:

  • (Boolean)


46
47
48
# File 'app/models/collavre/creative.rb', line 46

def github_markdown?
  data.is_a?(Hash) && data.dig("source", "type") == "github_markdown"
end

#inbox?Boolean

Returns:

  • (Boolean)


40
41
42
# File 'app/models/collavre/creative.rb', line 40

def inbox?
  data&.dig("kind") == "inbox"
end

#main_topic(fallback_user: user) ⇒ Object



58
59
60
61
62
# File 'app/models/collavre/creative.rb', line 58

def main_topic(fallback_user: user)
  topics.find_or_create_by!(name: MAIN_TOPIC_NAME) do |topic|
    topic.user = fallback_user
  end
end

#progress_for_plan(tagged_ids) ⇒ Object



204
205
206
# File 'app/models/collavre/creative.rb', line 204

def progress_for_plan(tagged_ids)
  progress_service.progress_for_plan(tagged_ids)
end

#progress_for_tags(tag_ids, user = Collavre.current_user) ⇒ Object



200
201
202
# File 'app/models/collavre/creative.rb', line 200

def progress_for_tags(tag_ids, user = Collavre.current_user)
  progress_service.progress_for_tags(tag_ids, user)
end

#prompt_for(user) ⇒ Object



190
191
192
193
194
195
196
197
198
# File 'app/models/collavre/creative.rb', line 190

def prompt_for(user)
  comments
    .where(private: true, user: user)
    .where("content LIKE ?", "> %")
    .order(created_at: :desc)
    .first
    &.content
    &.sub(/\A>\s*/i, "")
end

#subtree_idsObject

Compatibility helper: ancestry gem exposes ‘subtree_ids`, while closure_tree typically uses `self_and_descendants`.



181
182
183
# File 'app/models/collavre/creative.rb', line 181

def subtree_ids
  self_and_descendants.pluck(:id)
end

#system_topic(fallback_user: user) ⇒ Object

Find or create the “System” topic for this inbox creative. Re-creates it if the user deletes it.



52
53
54
55
56
# File 'app/models/collavre/creative.rb', line 52

def system_topic(fallback_user: user)
  topics.find_or_create_by!(name: SYSTEM_TOPIC_NAME) do |topic|
    topic.user = fallback_user
  end
end

#to_partial_pathObject

Use non-namespaced partial path for backward compatibility



9
10
11
# File 'app/models/collavre/creative.rb', line 9

def to_partial_path
  "creatives/creative"
end

#unarchive!Object



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'app/models/collavre/creative.rb', line 251

def unarchive!
  self.class.transaction do
    # Unarchive self and descendants
    self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)

    # If this is a linked creative, also unarchive the origin and its descendants
    origin = effective_origin(Set.new)
    if origin != self
      origin.self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)
      # Unarchive all other linked creatives of the origin
      origin.linked_creatives.where.not(id: id).find_each do |linked|
        linked.self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)
      end
    end

    # Also unarchive any linked creatives that point to this one
    linked_creatives.find_each do |linked|
      linked.self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)
    end

    reload
    parent&.reload
    Collavre::Creatives::ProgressService.new(parent).update_progress_from_children! if parent
  end
end

#update_parent_progressObject



208
209
210
211
212
213
214
215
216
217
# File 'app/models/collavre/creative.rb', line 208

def update_parent_progress
  progress_service.update_parent_progress!

  if saved_change_to_parent_id?
    old_parent_id = saved_change_to_parent_id[0]
    if old_parent_id && (old_parent = Creative.find_by(id: old_parent_id))
      Collavre::Creatives::ProgressService.new(old_parent).update_progress_from_children!
    end
  end
end