Class: Collavre::Creative
- Inherits:
-
ApplicationRecord
- Object
- ApplicationRecord
- ApplicationRecord
- Collavre::Creative
- 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
- BUILTIN_RESERVED_METADATA_KEYS =
Reserved metadata key registry — engines register their own namespaces so
update_metadatapreserves them without core naming vendor-specific keys. %w[markdown_source content_type editor].freeze
- SYSTEM_TOPIC_NAME =
"System"- MAIN_TOPIC_NAME =
"Main"- CONTENT_TOPIC_NAME =
"Content"
Constants included from Permissible
Permissible::PERMISSION_INVALIDATING_ATTRIBUTES
Instance Attribute Summary collapse
-
#filtered_progress ⇒ Object
Returns the value of attribute filtered_progress.
-
#skip_read_only_source_validation ⇒ Object
Bypass the read-only-source guard for a single save (used by the vendor sync services that legitimately write the synced content into core).
Class Method Summary collapse
-
.inbox_for(user) ⇒ Object
Find or create the inbox creative for a given user.
-
.read_only_source_types ⇒ Object
------------------------------------------------------------------------ Read-only source registry — vendor engines register the
data.source.typevalues whose content is owned by an external system (e.g. a synced GitHub repository) and therefore must not be edited in-app. - .register_read_only_source(type) ⇒ Object
- .register_reserved_metadata_key(key) ⇒ Object
- .registered_reserved_metadata_keys ⇒ Object
- .reserved_metadata_keys ⇒ Object
-
.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).
Instance Method Summary collapse
- #archive! ⇒ Object
-
#archived? ⇒ Boolean
--- Archive ---.
- #children ⇒ Object
- #content_topic(fallback_user: user) ⇒ Object
-
#context_creatives ⇒ Object
Returns context creatives (excludes self to avoid duplication).
-
#context_ids ⇒ Object
--- Context IDs --- Returns the directly-configured context creative IDs for this creative.
-
#disabled_context_ids ⇒ Object
Returns the directly-configured disabled context IDs for this creative.
-
#drop_trigger_enabled? ⇒ Boolean
--- Drop Trigger ---.
-
#effective_context_ids(visited_ids = Set.new) ⇒ Object
Returns the effective context IDs: own + inherited from ancestors (deduplicated).
-
#effective_disabled_context_ids(visited_ids = Set.new) ⇒ Object
Returns the effective disabled context IDs: own + inherited from ancestors (deduplicated).
-
#github_markdown? ⇒ Boolean
GitHub-sourced content still needs a vendor-specific predicate for the comment view's inline-image rendering (a GitHub-only concern, distinct from the neutral read-only behavior above).
- #inbox? ⇒ Boolean
- #main_topic(fallback_user: user) ⇒ Object
- #progress_for_plan(tagged_ids) ⇒ Object
- #progress_for_tags(tag_ids, user = Collavre.current_user) ⇒ Object
- #prompt_for(user) ⇒ Object
-
#read_only_source? ⇒ Boolean
Whether this creative's description is owned by an external, registered source and therefore read-only in-app.
-
#source_type ⇒ Object
The registered source identifier for this creative, or nil when the description is authored in-app.
-
#subtree_ids ⇒ Object
Compatibility helper: ancestry gem exposes
subtree_ids, while closure_tree typically usesself_and_descendants. -
#system_topic(fallback_user: user) ⇒ Object
Find or create the "System" topic for this inbox creative.
-
#to_partial_path ⇒ Object
Use non-namespaced partial path for backward compatibility.
- #unarchive! ⇒ Object
- #update_parent_progress ⇒ Object
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
#attachment_node_html, #attachments_embeddable?, #creative_snippet, #effective_description, #embed_attachment_blob!, #remove_attachment!
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_progress ⇒ Object
Returns the value of attribute filtered_progress.
138 139 140 |
# File 'app/models/collavre/creative.rb', line 138 def filtered_progress @filtered_progress end |
#skip_read_only_source_validation ⇒ Object
Bypass the read-only-source guard for a single save (used by the vendor sync services that legitimately write the synced content into core).
82 83 84 |
# File 'app/models/collavre/creative.rb', line 82 def skip_read_only_source_validation @skip_read_only_source_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.
126 127 128 129 130 131 132 133 134 135 136 |
# File 'app/models/collavre/creative.rb', line 126 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 |
.read_only_source_types ⇒ Object
Read-only source registry — vendor engines register the data.source.type
values whose content is owned by an external system (e.g. a synced GitHub
repository) and therefore must not be edited in-app. Core enforces the
read-only behavior via read_only_source? without naming any vendor.
33 34 35 |
# File 'app/models/collavre/creative.rb', line 33 def read_only_source_types @read_only_source_types ||= Set.new end |
.register_read_only_source(type) ⇒ Object
37 38 39 |
# File 'app/models/collavre/creative.rb', line 37 def register_read_only_source(type) read_only_source_types << type.to_s end |
.register_reserved_metadata_key(key) ⇒ Object
19 20 21 |
# File 'app/models/collavre/creative.rb', line 19 def (key) << key.to_s unless .include?(key.to_s) end |
.registered_reserved_metadata_keys ⇒ Object
15 16 17 |
# File 'app/models/collavre/creative.rb', line 15 def @registered_reserved_metadata_keys ||= [] end |
.reserved_metadata_keys ⇒ Object
23 24 25 |
# File 'app/models/collavre/creative.rb', line 23 def (BUILTIN_RESERVED_METADATA_KEYS + ).freeze 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.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'app/models/collavre/creative.rb', line 155 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
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'app/models/collavre/creative.rb', line 285 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 ---
281 282 283 |
# File 'app/models/collavre/creative.rb', line 281 def archived? archived_at.present? end |
#children ⇒ Object
246 247 248 249 |
# File 'app/models/collavre/creative.rb', line 246 def children # better not override this method, use children_with_permission instead or linked_children super end |
#content_topic(fallback_user: user) ⇒ Object
118 119 120 121 122 |
# File 'app/models/collavre/creative.rb', line 118 def content_topic(fallback_user: user) topics.find_or_create_by!(name: CONTENT_TOPIC_NAME) do |topic| topic.user = fallback_user end end |
#context_creatives ⇒ Object
Returns context creatives (excludes self to avoid duplication).
234 235 236 237 238 |
# File 'app/models/collavre/creative.rb', line 234 def context_creatives ids = effective_context_ids ids -= [ id ] Creative.where(id: ids) end |
#context_ids ⇒ Object
--- Context IDs --- Returns the directly-configured context creative IDs for this creative.
204 205 206 |
# File 'app/models/collavre/creative.rb', line 204 def context_ids Array(data&.dig("context_ids")) end |
#disabled_context_ids ⇒ Object
Returns the directly-configured disabled context IDs for this creative.
209 210 211 |
# File 'app/models/collavre/creative.rb', line 209 def disabled_context_ids Array(data&.dig("disabled_context_ids")) end |
#drop_trigger_enabled? ⇒ Boolean
--- Drop Trigger ---
198 199 200 |
# File 'app/models/collavre/creative.rb', line 198 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).
214 215 216 217 218 219 220 221 |
# File 'app/models/collavre/creative.rb', line 214 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).
224 225 226 227 228 229 230 231 |
# File 'app/models/collavre/creative.rb', line 224 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
GitHub-sourced content still needs a vendor-specific predicate for the comment view's inline-image rendering (a GitHub-only concern, distinct from the neutral read-only behavior above).
100 101 102 |
# File 'app/models/collavre/creative.rb', line 100 def github_markdown? source_type == "github_markdown" end |
#inbox? ⇒ Boolean
76 77 78 |
# File 'app/models/collavre/creative.rb', line 76 def inbox? data&.dig("kind") == "inbox" end |
#main_topic(fallback_user: user) ⇒ Object
112 113 114 115 116 |
# File 'app/models/collavre/creative.rb', line 112 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
265 266 267 |
# File 'app/models/collavre/creative.rb', line 265 def progress_for_plan(tagged_ids) progress_service.progress_for_plan(tagged_ids) end |
#progress_for_tags(tag_ids, user = Collavre.current_user) ⇒ Object
261 262 263 |
# File 'app/models/collavre/creative.rb', line 261 def (tag_ids, user = Collavre.current_user) progress_service.(tag_ids, user) end |
#prompt_for(user) ⇒ Object
251 252 253 254 255 256 257 258 259 |
# File 'app/models/collavre/creative.rb', line 251 def prompt_for(user) comments .where(private: true, user: user) .where("content LIKE ?", "> %") .order(created_at: :desc) .first &.content &.sub(/\A>\s*/i, "") end |
#read_only_source? ⇒ Boolean
Whether this creative's description is owned by an external, registered source and therefore read-only in-app.
92 93 94 95 |
# File 'app/models/collavre/creative.rb', line 92 def read_only_source? type = source_type type.present? && self.class.read_only_source_types.include?(type) end |
#source_type ⇒ Object
The registered source identifier for this creative, or nil when the description is authored in-app.
86 87 88 |
# File 'app/models/collavre/creative.rb', line 86 def source_type data.is_a?(Hash) ? data.dig("source", "type") : nil end |
#subtree_ids ⇒ Object
Compatibility helper: ancestry gem exposes subtree_ids, while
closure_tree typically uses self_and_descendants.
242 243 244 |
# File 'app/models/collavre/creative.rb', line 242 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.
106 107 108 109 110 |
# File 'app/models/collavre/creative.rb', line 106 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_path ⇒ Object
Use non-namespaced partial path for backward compatibility
43 44 45 |
# File 'app/models/collavre/creative.rb', line 43 def to_partial_path "creatives/creative" end |
#unarchive! ⇒ Object
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'app/models/collavre/creative.rb', line 312 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_progress ⇒ Object
269 270 271 272 273 274 275 276 277 278 |
# File 'app/models/collavre/creative.rb', line 269 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 |