Skip to content

AAP-68986 Move activity stream registry to setting#951

Open
AlanCoding wants to merge 3 commits into
ansible:develfrom
AlanCoding:auditable_model_cls
Open

AAP-68986 Move activity stream registry to setting#951
AlanCoding wants to merge 3 commits into
ansible:develfrom
AlanCoding:auditable_model_cls

Conversation

@AlanCoding

@AlanCoding AlanCoding commented Feb 24, 2026

Copy link
Copy Markdown
Member

Description

Technical refactor item:

Making activitystream app follow the philosophy of "outside looking in" regarding handling of the ultimate app that is using it. Everything in activitystream uses GenericForeignKey, and as things stand, it is completely consistent with this philosophy.

A concern was expressed that adding a field to AditableModel will trigger migrations in apps. That is a question that I don't think should exist, and it won't with these changes.

I also find this to be something that will ease its introduction to AWX and other apps. We do not want to add more parent classes to the models there.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Test update
  • Refactoring (no functional changes)
  • Development environment change
  • Configuration change

Self-Review Checklist

  • I have performed a self-review of my Claude's code
  • I have added relevant comments to complex code sections
  • I have updated documentation where needed
  • I have considered the security impact of these changes
  • I have considered performance implications
  • I have thought about error handling and edge cases
  • I have tested the changes in my local environment

Note

Medium Risk
Changes how activity-stream signals are connected (now setting-driven with a runtime registry), so misconfiguration could silently stop auditing or change related links. Also adds a custom non-cascading GenericRelation, affecting delete behavior around audit records.

Overview
Shifts activity-stream tracking from AuditableModel inheritance to an explicit ACTIVITY_STREAM_MODELS setting: ActivitystreamConfig.ready() now connects create/update/delete/m2m signals only for configured models, while still auto-detecting AuditableModel subclasses with deprecation warnings.

Adds public helpers get_activity_stream_entries() / get_activity_stream_related_url() and automatically injects an activity_stream related link for registered models via CommonModel.related_fields(). Introduces activity_stream_entries() (a non-cascading GenericRelation) so models can optionally expose a reverse relation without risking audit entry deletion when the parent object is deleted.

Updates test app models, OAuth2 models, docs, and tests to remove required AuditableModel inheritance, rely on the new setting-based registration, and validate the new reverse relation and non-cascading delete behavior.

Reviewed by Cursor Bugbot for commit 91a2fad. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Activity stream tracking now uses configuration-based model registration via ACTIVITY_STREAM_MODELS setting instead of class inheritance
    • New helper functions for querying activity stream entries
    • Models can include reverse activity stream relations for direct entry access
  • Deprecations

    • AuditableModel inheritance approach is now deprecated; migrate to ACTIVITY_STREAM_MODELS setting
  • Changes

    • OAuth2 models no longer automatically tracked in activity stream by default

@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The pull request refactors the activity stream system from an inheritance-based model (using AuditableModel) to a configuration-based approach via ACTIVITY_STREAM_MODELS setting. New helper functions get_activity_stream_entries() and get_activity_stream_related_url() replace direct inline logic. OAuth2 models are decoupled from activity stream tracking. Deprecation warnings guide migration of subclasses away from AuditableModel.

Changes

Cohort / File(s) Summary
Activity Stream Core
ansible_base/activitystream/__init__.py, ansible_base/activitystream/apps.py, ansible_base/activitystream/models/entry.py, ansible_base/activitystream/signals.py
Added activity_stream_entries() function returning custom GenericRelation. Introduced get_activity_stream_entries(instance) and get_activity_stream_related_url(instance) helpers in app config. Updated AuditableModel to emit DeprecationWarning on subclassing and delegate to new helpers. Made signal handler registration config-driven with backward-compatible scan emitting deprecation warnings. Updated signal docstrings to reference ACTIVITY_STREAM_MODELS setting.
Integration & Common Layers
ansible_base/lib/abstract_models/common.py
Added conditional import and integration of get_activity_stream_related_url() when activity stream app is installed, merging results into related-fields response.
OAuth2 Provider Models
ansible_base/oauth2_provider/models/access_token.py, ansible_base/oauth2_provider/models/application.py, ansible_base/oauth2_provider/models/id_token.py, ansible_base/oauth2_provider/models/refresh_token.py
Removed conditional AuditableModel inheritance from all four OAuth2 models. Each now inherits only from CommonModel (or NamedCommonModel) and their respective abstract OAuth2 provider base classes.
Test Configuration
test_app/defaults.py, test_app/models.py
Added ACTIVITY_STREAM_MODELS setting listing test app and OAuth2 provider models for signal registration. Removed AuditableModel inheritance from User, Animal, City, and SecretColor models. Added explicit activity_stream = activity_stream_entries() field to Animal.
Activity Stream Tests
test_app/tests/activitystream/models/test_entry.py, test_app/tests/activitystream/test_api.py, test_app/tests/activitystream/test_signals.py
Updated tests to use get_activity_stream_entries() helper instead of .activity_stream_entries related manager. Added test_activitystream_generic_relation to validate the new reverse relation. Refactored attribute-testing pattern from AuditableModel defaults to signal-handling defaults via getattr(). Updated mocking to use create=True for non-existent patched attributes.
OAuth2 Provider Tests
test_app/tests/oauth2_provider/test_authentication.py, test_app/tests/oauth2_provider/views/test_application.py
Updated OAuth2 tests to retrieve activity stream entries via get_activity_stream_entries() instead of instance's .activity_stream_entries attribute.
Documentation
docs/apps/activitystream.md
Replaced inheritance-based setup guidance with configuration-based approach. Added documentation for ACTIVITY_STREAM_MODELS setting and get_activity_stream_entries() query helper. Introduced guidance for optional model-level activity_stream reverse relation using activity_stream_entries() helper.

Sequence Diagram(s)

sequenceDiagram
    participant Settings as Settings<br/>(ACTIVITY_STREAM_MODELS)
    participant AppReady as ActivitystreamConfig<br/>.ready()
    participant Registry as Registered<br/>Models
    participant Signals as Django<br/>Signals
    participant Model as Tracked<br/>Model

    Note over Settings,Model: New Configuration-Based Flow
    Settings->>AppReady: ACTIVITY_STREAM_MODELS list
    AppReady->>Registry: apps.get_model() per tuple
    Registry->>AppReady: Model class reference
    AppReady->>Signals: connect(post_save, post_delete, m2m_changed)
    Signals->>Model: Subscribe to create/update/delete/m2m
    Model->>Signals: Emit signal on modification
    Signals->>Registry: Entry created via signal handler

    Note over Settings,Model: Old Inheritance-Based Flow (Deprecated)
    Model-->>Model: class MyModel(AuditableModel)
    Model-->>AppReady: DeprecationWarning emitted on subclass
    AppReady->>Signals: Scan & connect for discovered subclasses
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main refactoring objective: moving the activity stream model registry from inherited AuditableModel inheritance to a configuration-based setting approach.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AlanCoding AlanCoding changed the title Move activity stream registry to setting, co-authored with Claude Opus 4.6 AAP-68986 Move activity stream registry to setting, co-authored with Claude Opus 4.6 Mar 19, 2026
@AlanCoding AlanCoding force-pushed the auditable_model_cls branch from dc69f56 to c365b16 Compare March 19, 2026 13:40
@AlanCoding AlanCoding marked this pull request as ready for review March 20, 2026 13:00
def bulk_related_objects(self, objs, using='default'):
# Return an empty queryset so Django's deletion collector
# never picks up (and deletes) the related entries.
return self.remote_field.model._base_manager.db_manager(using).none()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NonCascadingGenericRelation override is dead code in Django 5.2

High Severity

NonCascadingGenericRelation overrides bulk_related_objects to prevent cascade deletion of audit entries, but this method was removed from Django's deletion collector in Django 1.9. Since this project uses Django 5.2, the override is never invoked — the GenericRelation will still cascade-delete all related Entry objects when the parent is deleted. This silently destroys audit trail data, contradicting the docstring and the documented guarantee that "activity stream records must survive object deletion." Any model using activity_stream_entries() (e.g. Animal) will lose its entire activity stream history on deletion.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not correct. Wrote a test to confirm, will push in another commit. The case here is that you do animal.delete(), after this the activity stream entries are not lost. The distinction is apparently that it is called on for field in model._meta.private_fields, and this is still necessary.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
docs/apps/activitystream.md (1)

34-51: Consider fixing heading hierarchy for consistent document structure.

The heading "Adding an activity_stream relation to your model" uses #### (h4) but follows a section with ## (h2), skipping h3. This creates inconsistent document structure.

📝 Suggested fix
-#### Adding an `activity_stream` relation to your model
+### Adding an `activity_stream` relation to your model
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/apps/activitystream.md` around lines 34 - 51, The heading "Adding an
`activity_stream` relation to your model" is at level #### which skips H3 under
the preceding H2; change that heading to ### to restore correct hierarchy and
document structure so the section introducing activity_stream_entries, the
MyModel example and the activity_stream reverse relation are nested properly
under the parent H2.
ansible_base/activitystream/apps.py (1)

77-104: Consider using stacklevel=2 for accurate deprecation warning attribution.

On line 101, stacklevel=1 points to the warnings.warn call itself. Since this is called from within the ready() method during app initialization (not from user code directly subclassing), stacklevel=1 may actually be appropriate here as the warning is informational about the model rather than pointing to a specific call site. However, if you want the warning to point to where the model is defined, you'd need a different approach since this runs at app startup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ansible_base/activitystream/apps.py` around lines 77 - 104, The deprecation
warning uses warnings.warn(..., stacklevel=1) inside the app initialization,
which attributes the warning to the warn call itself; update the warnings.warn
invocation in the ready() logic that handles AuditableModel detection (where msg
is built and warnings.warn(msg, DeprecationWarning, stacklevel=1) is called) to
use stacklevel=2 so the reported stack frame points one level up (closer to
where the user's model is defined) and keep the rest of the call (message and
DeprecationWarning) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ansible_base/activitystream/apps.py`:
- Around line 77-104: The deprecation warning uses warnings.warn(...,
stacklevel=1) inside the app initialization, which attributes the warning to the
warn call itself; update the warnings.warn invocation in the ready() logic that
handles AuditableModel detection (where msg is built and warnings.warn(msg,
DeprecationWarning, stacklevel=1) is called) to use stacklevel=2 so the reported
stack frame points one level up (closer to where the user's model is defined)
and keep the rest of the call (message and DeprecationWarning) unchanged.

In `@docs/apps/activitystream.md`:
- Around line 34-51: The heading "Adding an `activity_stream` relation to your
model" is at level #### which skips H3 under the preceding H2; change that
heading to ### to restore correct hierarchy and document structure so the
section introducing activity_stream_entries, the MyModel example and the
activity_stream reverse relation are nested properly under the parent H2.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7912aa5f-04b5-4419-8eb9-804440263687

📥 Commits

Reviewing files that changed from the base of the PR and between fe72043 and c365b16.

📒 Files selected for processing (17)
  • ansible_base/activitystream/__init__.py
  • ansible_base/activitystream/apps.py
  • ansible_base/activitystream/models/entry.py
  • ansible_base/activitystream/signals.py
  • ansible_base/lib/abstract_models/common.py
  • ansible_base/oauth2_provider/models/access_token.py
  • ansible_base/oauth2_provider/models/application.py
  • ansible_base/oauth2_provider/models/id_token.py
  • ansible_base/oauth2_provider/models/refresh_token.py
  • docs/apps/activitystream.md
  • test_app/defaults.py
  • test_app/models.py
  • test_app/tests/activitystream/models/test_entry.py
  • test_app/tests/activitystream/test_api.py
  • test_app/tests/activitystream/test_signals.py
  • test_app/tests/oauth2_provider/test_authentication.py
  • test_app/tests/oauth2_provider/views/test_application.py

Add back in AuditableModel class

Code cleanup that I should not have had to ask Claude to do but I did

Do not leave relationship around on external models

Fix generic relation
@AlanCoding AlanCoding force-pushed the auditable_model_cls branch from c365b16 to cae836c Compare April 6, 2026 12:16
@AlanCoding AlanCoding requested a review from huffmanca April 6, 2026 12:16
"""Ensure signal handlers fall back to the correct defaults when a model lacks an attribute."""
# SecretColor doesn't define these attributes (except what it inherits),
# so getattr with defaults should match expectations.
assert getattr(SecretColor, attribute, expected_default) == expected_default

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tautological test always passes, verifies nothing meaningful

Low Severity

test_signal_handler_uses_correct_defaults is tautological — it uses expected_default as both the getattr fallback and the assertion target. Since SecretColor (now inheriting plain models.Model) lacks all four tested attributes, getattr(SecretColor, attribute, expected_default) always returns expected_default, making the assertion expected_default == expected_default — trivially true for every parameterized case. The original test meaningfully verified that AuditableModel defined these attributes with the correct values; the replacement verifies nothing about the signal handler defaults it claims to test.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cae836c. Configure here.

@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown

DVCS PR Check Results:

PR appears valid (JIRA key(s) found)

@AlanCoding AlanCoding changed the title AAP-68986 Move activity stream registry to setting, co-authored with Claude Opus 4.6 AAP-68986 Move activity stream registry to setting Apr 6, 2026
@AlanCoding

Copy link
Copy Markdown
Member Author

@jeffh-oss @huffmanca In #956 it was requested to expedite that fix and not couple this refactoring in with it.

This still does not undo the key thing (subclasses RBAC app models with activitystream app models), because that requires changing the configuration in aap-gateway (https://github.com/ansible-automation-platform/aap-gateway/pull/1297), and then coming back here and removing it.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 91a2fad. Configure here.

if 'ansible_base.activitystream' in settings.INSTALLED_APPS:
from ansible_base.activitystream.apps import get_activity_stream_related_url

response.update(get_activity_stream_related_url(self))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate activity_stream URL generation for backward-compat models

Low Severity

For models that still inherit from AuditableModel (backward compat path), related_fields() calls self.extra_related_fields(request) on line 253–254 which invokes get_activity_stream_related_url, and then the new block on lines 256–259 calls get_activity_stream_related_url a second time. Both produce the same {'activity_stream': url} dict for registered models, making the AuditableModel.extra_related_fields method entirely redundant for any model that flows through CommonModel.related_fields().

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 91a2fad. Configure here.

@sonarqubecloud

sonarqubecloud Bot commented Apr 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant