Skip to content

Improve is_flag_active_for_request#995

Open
Charl1996 wants to merge 4 commits intomainfrom
improve-is_flag_active_for_request
Open

Improve is_flag_active_for_request#995
Charl1996 wants to merge 4 commits intomainfrom
improve-is_flag_active_for_request

Conversation

@Charl1996
Copy link
Copy Markdown
Contributor

@Charl1996 Charl1996 commented Feb 18, 2026

Technical Summary

This PR improves the is_flag_active_for_request method on Flag model by considering the objects from the request instead of what flags the user has enabled.

Currently is_flag_active_for_request is looking at all the flags active for the user (which will be a constant list regardless of where the user navigates on the platform) and simply checking if the specified flag is in this list - this means it's not request-specific (apart from pulling the user out of the request), so it's not considering whether the user is now looking at another program for instance which the flag is not active for (but currently it will think it is because the active_flags_for_user returns a constant list).

What I think should happen is that we look at the request to get the context of which org/program/opportunity we're looking at and then checking whether the flag is active for those objects.

Safety Assurance

Safety story

Tested locally

Automated test coverage

Need to add some tests for this.

QA Plan

N.A

Labels & Review

  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@Charl1996 Charl1996 requested a review from zandre-eng February 18, 2026 08:29
Comment thread commcare_connect/flags/models.py Outdated
program = opportunity.managedopportunity.program if opportunity and opportunity.managed else None
organization = getattr(request, "org", None)

filters = models.Q(users=user)
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.

Apart from filtering for the user, we'll also want to short-circuit to returning True if the flag is enabled for staff/superusers and the user is one of those two.

Also, as a separate question, do we want to filter on org/opp/program if the user filter matches? I feel like if we explicitly enable it for a user in the flag then it should be enabled across all orgs/opps/programs for that users. What do you think?

Copy link
Copy Markdown
Contributor Author

@Charl1996 Charl1996 Feb 18, 2026

Choose a reason for hiding this comment

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

Apart from filtering for the user, we'll also want to short-circuit to returning True if the flag is enabled for staff/superusers and the user is one of those two.

I'll update

Also, as a separate question, do we want to filter on org/opp/program if the user filter matches? I feel like if we explicitly enable it for a user in the flag then it should be enabled across all orgs/opps/programs for that users. What do you think?

Since it's an OR operation, does it matter? If it's enabled for a particular user that will still evaluate to True

@Charl1996 Charl1996 marked this pull request as ready for review February 18, 2026 10:17
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

Walkthrough

The is_flag_active_for_request method in commcare_connect/flags/models.py was changed to build a dynamic composite OR filter based on request-derived context (opportunity, managed opportunity → program, organization) and user roles/visibility scopes (staff, superuser, everyone, organization, opportunity, program) instead of delegating to active_flags_for_user(...).filter(name=...). Tests were expanded with a RequestFactory helper and a comprehensive test class exercising flag activation across unauthenticated users, per-user flags, organization/opportunity/program contexts, and managed opportunities.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • zandre-eng
  • calellowitz
  • pxwxnvermx
  • hemant10yadav
  • ajeety4
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title 'Improve is_flag_active_for_request' accurately describes the main change, which refactors the is_flag_active_for_request method to consider request context instead of user's constant flag list.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining the problem with the current implementation and the solution being implemented through request context consideration.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-is_flag_active_for_request

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
commcare_connect/flags/tests/test_flag_models.py (1)

205-212: Consider adding a test for when opportunity.managed is True but managedopportunity relation is missing.

This would exercise the defensive path (or expose the crash) discussed in the model review. A test like this would confirm the method handles corrupt/inconsistent data gracefully:

def test_managed_opportunity_with_missing_relation(self):
    user = UserFactory()
    opportunity = OpportunityFactory(managed=True)
    # No ManagedOpportunity created for this opportunity
    flag = FlagFactory()
    request = self._make_request(user=user, opportunity=opportunity)
    # Should not raise; should return False
    assert Flag.is_flag_active_for_request(request, flag.name) is False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commcare_connect/flags/tests/test_flag_models.py` around lines 205 - 212, Add
a test that covers the case where an Opportunity has managed=True but lacks a
ManagedOpportunity relation to ensure Flag.is_flag_active_for_request handles
this corrupt/inconsistent state without raising: create a user and an
Opportunity via OpportunityFactory(managed=True) without creating a
ManagedOpportunity, create a Flag via FlagFactory (optionally attach programs if
needed), build the request with the existing _make_request(user=...,
opportunity=...), and assert that Flag.is_flag_active_for_request(request,
flag.name) returns False and does not raise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commcare_connect/flags/models.py`:
- Around line 66-83: Replace the direct reverse-relation access
opportunity.managedopportunity with a defensive getattr call: obtain opportunity
via getattr(request, "opportunity", None) (already present), then get
managed_opportunity = getattr(opportunity, "managedopportunity", None) and set
program = managed_opportunity.program if opportunity and opportunity.managed and
managed_opportunity else None; update the symbol references in this method
(opportunity, managedopportunity, program) so missing ManagedOpportunity
instances do not raise RelatedObjectDoesNotExist.

---

Nitpick comments:
In `@commcare_connect/flags/tests/test_flag_models.py`:
- Around line 205-212: Add a test that covers the case where an Opportunity has
managed=True but lacks a ManagedOpportunity relation to ensure
Flag.is_flag_active_for_request handles this corrupt/inconsistent state without
raising: create a user and an Opportunity via OpportunityFactory(managed=True)
without creating a ManagedOpportunity, create a Flag via FlagFactory (optionally
attach programs if needed), build the request with the existing
_make_request(user=..., opportunity=...), and assert that
Flag.is_flag_active_for_request(request, flag.name) returns False and does not
raise.

Comment thread commcare_connect/flags/models.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@commcare_connect/flags/models.py`:
- Around line 67-73: The code correctly uses getattr to defensively access the
reverse OneToOne relation: keep the pattern in the block that reads opportunity
= getattr(request, "opportunity", None) and then uses managed_opp =
getattr(opportunity, "managedopportunity", None) to set program =
managed_opp.program only when managed_opp is truthy; no change required other
than ensuring this exact guarded access remains in the opportunity → managed_opp
→ program flow (symbols: opportunity, managed_opp, program, request).

Copy link
Copy Markdown
Contributor

@mkangia mkangia left a comment

Choose a reason for hiding this comment

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

Thanks @Charl1996

I am surprised we had this setup incorrectly.
I don't fully understand the models being used so I can share an approval but had a couple of queries for my own understanding.
I see you have an approval already, considering it's related to authorization, would be good to have at least one more.

if user.is_superuser:
filters |= models.Q(superusers=True)
if organization:
filters |= models.Q(organizations__id=organization.id)
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.

so this is confirming if the ID of the "organization" from the request is one of the organization IDs added for the flag?

if opportunity and opportunity.managed:
managed_opp = getattr(opportunity, "managedopportunity", None)
if managed_opp:
program = managed_opp.program
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.

not sure I fully understand the path from opportunity to program.

During this block, can we assume that the user has access to the managed opp and the program if they have opportunity on the request object?

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.

5 participants