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.



86
87
88
# File 'app/models/collavre/creative.rb', line 86

def filtered_progress
  @filtered_progress
end

#skip_github_validationObject

Returns the value of attribute skip_github_validation.



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

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.



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

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.



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

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



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

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)


222
223
224
# File 'app/models/collavre/creative.rb', line 222

def archived?
  archived_at.present?
end

#childrenObject



187
188
189
190
# File 'app/models/collavre/creative.rb', line 187

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

#content_topic(fallback_user: user) ⇒ Object



66
67
68
69
70
# File 'app/models/collavre/creative.rb', line 66

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).



175
176
177
178
179
# File 'app/models/collavre/creative.rb', line 175

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.



145
146
147
# File 'app/models/collavre/creative.rb', line 145

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

#disabled_context_idsObject

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



150
151
152
# File 'app/models/collavre/creative.rb', line 150

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

#drop_trigger_enabled?Boolean

— Drop Trigger —

Returns:

  • (Boolean)


139
140
141
# File 'app/models/collavre/creative.rb', line 139

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).



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

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).



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

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)


48
49
50
# File 'app/models/collavre/creative.rb', line 48

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

#inbox?Boolean

Returns:

  • (Boolean)


42
43
44
# File 'app/models/collavre/creative.rb', line 42

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

#main_topic(fallback_user: user) ⇒ Object



60
61
62
63
64
# File 'app/models/collavre/creative.rb', line 60

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



206
207
208
# File 'app/models/collavre/creative.rb', line 206

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

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



202
203
204
# File 'app/models/collavre/creative.rb', line 202

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

#prompt_for(user) ⇒ Object



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

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`.



183
184
185
# File 'app/models/collavre/creative.rb', line 183

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.



54
55
56
57
58
# File 'app/models/collavre/creative.rb', line 54

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



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

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



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

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