Skip to content

Conversation

@jjackson
Copy link
Member

This PR adds Expression of Interest (EOI) pages and solicitation functionality to the CommCare Connect platform.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 7, 2025

Walkthrough

This pull request introduces three major new features to CommCare Connect: (1) a Solicitations app for managing Expressions of Interest (EOIs) and Request for Proposals (RFPs) with questions, responses, reviews, and attachments; (2) an Audit app for conducting audits with templates, data extraction from CommCare HQ via APIs and Superset, result tracking, and assessments; (3) a Labs OAuth authentication system providing session-based login without database persistence. Supporting changes include a new GitHub Actions workflow for AWS Fargate deployment, field constraints on existing models, database migrations, extensive fixtures and sample data, management commands for data loading and setup, comprehensive test suites, documentation, and configuration updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Solicitations models, views, and forms (commcare_connect/solicitations/models.py, views.py, forms.py) — new domain logic with status transitions, permissions, draft/submit workflows, and dynamic form generation; requires validation of access control patterns and form state management
  • Audit data services (commcare_connect/audit/services/audit_creator.py, data_loader.py) — complex orchestration of data loading, granularity handling (combined/per_opp/per_flw), OAuth token management, and attachment downloads; verify correctness of session creation logic and date range handling
  • Labs OAuth implementation (commcare_connect/labs/) — security-critical components including token handling, PKCE flow, state validation, and session-based authentication without DB persistence; verify CSRF and token expiration protections
  • Database migrations (commcare_connect/*/migrations/) — multiple migrations with data transformations and schema changes; ensure they handle existing data safely and dependencies are correct
  • AWS deployment workflow (.github/workflows/deploy-labs.yml) — infrastructure automation including ECS task definition discovery, migrations, service deployment; verify proper error handling and rollback logic
  • Permission mixins and access control — verify solicitations and audit views properly enforce organization membership and role-based access across all endpoints
  • Form validation (commcare_connect/solicitations/forms.py) — date range validation, required field enforcement in draft vs. submission modes, and organization membership checks
  • Management commands — ensure idempotency and proper error handling for data loading and fixture setup across multiple commands

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the primary change: adding EOI pages and solicitation functionality.
Description check ✅ Passed Description is related to the changeset, mentioning Expression of Interest pages and solicitation functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
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: 31

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
commcare_connect/contrib/sites/migrations/0003_set_site_domain_and_name.py (1)

19-30: Remove redundant SQLite check.

The outer condition on line 19 already gates entry to the sequence sync block with connection.vendor != "sqlite", meaning the block is only entered when the vendor is NOT SQLite. The inner check on lines 28-30 testing for SQLite will never evaluate to true and is unreachable dead code.

Apply this diff to remove the redundant inner check:

-    if created and connection.vendor != "sqlite":
+    if created and connection.vendor != "sqlite":
         # We provided the ID explicitly when creating the Site entry, therefore the DB
         # sequence to auto-generate them wasn't used and is now out of sync. If we
         # don't do anything, we'll get a unique constraint violation the next time a
         # site is created.
         # To avoid this, we need to manually update DB sequence and make sure it's
         # greater than the maximum value.
         max_id = site_model.objects.order_by('-id').first().id
         with connection.cursor() as cursor:
-            # Skip sequence adjustment on SQLite where sequences are not used
-            if connection.vendor == "sqlite":
-                return
             cursor.execute("SELECT last_value from django_site_id_seq")
             (current_id,) = cursor.fetchone()
             if current_id <= max_id:
🧹 Nitpick comments (17)
commcare_connect/solicitations/instructions/rename-progress-summary.md (2)

1-1: Update the document date and consider archival or integration into main documentation.

This progress summary is dated 8/20/25 but we're now in November 2025—approximately 2.5 months have passed. The document should either be updated to reflect current status (particularly Phase 4 outcomes if work has progressed) or archived with a clear note about its historical scope.

  • Has Phase 4 (Testing & Documentation) been completed? If so, create a Phase 4 summary or update this document.
  • Should this file be moved to a historical archive or integrated into main project documentation (e.g., docs/architecture/solicitations.md)?

124-140: Consider tracking Phase 4 work in a separate, linked document or GitHub issues.

Phase 4 is currently outlined as unchecked tasks in this static document. For an active project, tracking these items in GitHub issues or a living roadmap document will make it easier for the team to iterate and stay aligned.

demo_decorator.py (2)

17-28: Add if __name__ == "__main__": guard for script execution.

The print statements and function calls execute immediately when the module is imported, causing side effects. Wrap the execution code in a main guard:

+if __name__ == "__main__":
-print("1. Defining the decorated function...")
+    print("1. Defining the decorated function...")


-@my_decorator
-def hello(name):
-    print(f"Hello, {name}!")
-    return f"Greeting for {name}"
+    @my_decorator
+    def hello(name):
+        print(f"Hello, {name}!")
+        return f"Greeting for {name}"


-print("2. Calling the decorated function...")
-result = hello("World")
-print(f"3. Result: {result}")
+    print("2. Calling the decorated function...")
+    result = hello("World")
+    print(f"3. Result: {result}")

1-1: Make file executable or remove shebang.

The shebang is present but the file lacks executable permissions. If this file is meant to be executed directly, add executable permissions; otherwise, remove the shebang.

#!/bin/bash
# Add executable permissions if this is intended as a standalone script
chmod +x demo_decorator.py
commcare_connect/solicitations/instructions/labs.md (1)

11-30: Add language identifier to fenced code block.

The ASCII diagram in the fenced code block should specify a language identifier for proper markdown rendering. Using text or plaintext would be appropriate for ASCII art.

Apply this diff:

-```
+```text
 ┌─────────────────────┐    ┌─────────────────────┐
 │  connect.dimagi.com │    │ labs.connect.dimagi │
 │                     │    │                     │
commcare_connect/static/js/datetime-utils.js (2)

7-19: Add error handling for invalid dates.

The code parses isoDateTime without validating the resulting Date object. Invalid datetime strings will create an Invalid Date object, and toLocaleString() will return "Invalid Date" as the text content.

Apply this diff to add validation:

     var isoDateTime = element.getAttribute('datetime');
     if (isoDateTime) {
       var date = new Date(isoDateTime);
+      if (isNaN(date.getTime())) {
+        continue;  // Skip invalid dates
+      }
       var localTime = date.toLocaleString('en-US', {
         year: 'numeric',

10-17: Consider using navigator.language for locale.

The locale is hardcoded to 'en-US', which may not match the user's preferred locale. While the comment mentions "user's local timezone" (which is correctly applied via toLocaleString), the format is always US-style.

If internationalization is a concern, consider:

-      var localTime = date.toLocaleString('en-US', {
+      var localTime = date.toLocaleString(navigator.language || 'en-US', {
         year: 'numeric',
commcare_connect/solicitations/instructions/user-organization-program-relationships.md (1)

9-11: Add language identifier to fenced code block.

The static analysis tool suggests adding a language identifier to the fenced code block. Since this is ASCII art representing relationships, consider using text as the language identifier.

Apply this diff:

-```
+```text
 User ←→ UserOrganizationMembership ←→ Organization ←→ Program

</blockquote></details>
<details>
<summary>commcare_connect/solicitations/instructions/implement-plan.md (1)</summary><blockquote>

`122-130`: **Add language specifiers to code blocks.**

Multiple code blocks throughout this file lack language specifiers (lines 122, 172, 188, 204, 219, 263, 273, 283), which degrades documentation readability and syntax highlighting.


Apply language specifiers to improve rendering:

```diff
-```
+```text
 Program Manager creates EOI → Organizations submit responses
 ↓
 ...
commcare_connect/solicitations/management/commands/test_dashboard_links.py (1)

44-46: Consider using spread operator for list concatenation.

While the current concatenation works, using the spread operator is more idiomatic in modern Python.

Apply this diff:

-        settings.ALLOWED_HOSTS = current_allowed_hosts + ["testserver"]
+        settings.ALLOWED_HOSTS = [*current_allowed_hosts, "testserver"]
commcare_connect/solicitations/tests/test_helpers.py (1)

150-165: Consider using _ for intentionally unused variable.

The static analysis tool correctly identifies that the error variable at line 155 is never used. Since the test intentionally ignores the error return value when verifying successful question creation, consider using _ to make this intention explicit.

Apply this diff to clarify the intent:

         success, error = process_solicitation_questions(questions_json, self.solicitation)
 
         # Should still succeed but create question with empty text
         assert success is True
         assert self.solicitation.questions.count() == 1

could become:

-        success, error = process_solicitation_questions(questions_json, self.solicitation)
+        success, _ = process_solicitation_questions(questions_json, self.solicitation)
 
         # Should still succeed but create question with empty text
         assert success is True
         assert self.solicitation.questions.count() == 1
commcare_connect/templates/solicitations/response_form.html (1)

382-390: Consider using CSS classes instead of inline styles for validation feedback.

The current approach directly manipulates inline styles for validation feedback. Using CSS classes would be more maintainable and align better with the existing Tailwind-style approach.

Consider refactoring to use CSS classes:

             requiredFields.forEach(field => {
                 if (!field.value.trim()) {
                     hasEmptyRequired = true;
-                    field.style.borderColor = '#ef4444';
-                    field.style.boxShadow = '0 0 0 1px #ef4444';
+                    field.classList.add('border-red-500', 'ring-1', 'ring-red-500');
                 } else {
-                    field.style.borderColor = '';
-                    field.style.boxShadow = '';
+                    field.classList.remove('border-red-500', 'ring-1', 'ring-red-500');
                 }
             });
commcare_connect/solicitations/helpers.py (1)

171-174: Consider optimizing total_responses calculation.

The current implementation annotates each solicitation and then sums the results in Python, which evaluates the entire queryset in memory. For large datasets, this could be inefficient.

Consider using database aggregation:

-    # Calculate total responses across all solicitations
-    annotated_solicitations = solicitations_queryset.annotate(
-        total_responses=Count("responses", filter=~Q(responses__status=SolicitationResponse.Status.DRAFT))
-    )
-    stats["total_responses"] = sum(s.total_responses or 0 for s in annotated_solicitations)
+    # Calculate total responses across all solicitations using database aggregation
+    from django.db.models import Sum
+    total_responses = solicitations_queryset.aggregate(
+        total=Count("responses", filter=~Q(responses__status=SolicitationResponse.Status.DRAFT))
+    )
+    stats["total_responses"] = total_responses["total"] or 0
commcare_connect/solicitations/views.py (4)

401-418: Consider wrapping solicitation save and question processing in a transaction.

If question processing fails after the solicitation is saved, you'll have a solicitation without questions. While the user can edit to add questions later, it's better to ensure atomicity.

+        from django.db import transaction
+
         # Save the solicitation first
-        response = super().form_valid(form)
-
-        # Process questions using appropriate helper
-        questions_data = self.request.POST.get("questions_data")
-        if is_edit:
-            success, error_message = update_solicitation_questions(questions_data, self.object)
-        else:
-            success, error_message = process_solicitation_questions(questions_data, self.object)
-
-        if not success:
-            if is_edit:
-                messages.warning(self.request, f"Questions could not be saved: {error_message}. Please try again.")
-            else:
-                messages.warning(
-                    self.request,
-                    f"Questions could not be saved: {error_message}. Please edit the solicitation to add questions.",
-                )
+        with transaction.atomic():
+            response = super().form_valid(form)
+
+            # Process questions using appropriate helper
+            questions_data = self.request.POST.get("questions_data")
+            if is_edit:
+                success, error_message = update_solicitation_questions(questions_data, self.object)
+            else:
+                success, error_message = process_solicitation_questions(questions_data, self.object)
+
+            if not success:
+                if is_edit:
+                    messages.warning(self.request, f"Questions could not be saved: {error_message}. Please try again.")
+                else:
+                    messages.warning(
+                        self.request,
+                        f"Questions could not be saved: {error_message}. Please edit the solicitation to add questions.",
+                    )
+                # Transaction will rollback, preventing inconsistent state
+                raise ValidationError(error_message)

983-988: Prefix unused variable with underscore.

The created variable from get_or_create is never used. Following Python conventions, prefix it with an underscore to indicate it's intentionally unused.

         # Get or create draft
-        draft, created = SolicitationResponse.objects.get_or_create(
+        draft, _created = SolicitationResponse.objects.get_or_create(
             solicitation=solicitation,
             organization=user_org,
             status=SolicitationResponse.Status.DRAFT,
             defaults={"submitted_by": request.user, "responses": {}},
         )

1012-1017: Prefix unused variable with underscore.

The created variable from get_or_create is never used. Following Python conventions, prefix it with an underscore to indicate it's intentionally unused.

     # Get or create draft response
-    draft, created = SolicitationResponse.objects.get_or_create(
+    draft, _created = SolicitationResponse.objects.get_or_create(
         solicitation=solicitation,
         organization=user_org,
         status=SolicitationResponse.Status.DRAFT,
         defaults={"submitted_by": request.user, "responses": {}},
     )

1115-1115: Use is not None instead of != None for consistency.

While the existing comparison works, using is not None is more Pythonic and consistent with other null checks in the codebase.

-            return self.request.org_membership != None or self.request.user.is_superuser  # noqa: E711
+            return self.request.org_membership is not None or self.request.user.is_superuser
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd5d270 and 4ef664a.

📒 Files selected for processing (71)
  • .github/workflows/deploy-labs.yml (1 hunks)
  • QUICK_START_GUIDE.md (1 hunks)
  • commcare_connect/contrib/sites/migrations/0003_set_site_domain_and_name.py (2 hunks)
  • commcare_connect/opportunity/migrations/0030_uservisit_location.py (1 hunks)
  • commcare_connect/opportunity/migrations/0083_alter_uservisit_location.py (1 hunks)
  • commcare_connect/opportunity/models.py (1 hunks)
  • commcare_connect/program/migrations/0001_initial.py (1 hunks)
  • commcare_connect/program/migrations/0002_alter_program_description.py (1 hunks)
  • commcare_connect/program/models.py (1 hunks)
  • commcare_connect/reports/views.py (1 hunks)
  • commcare_connect/solicitations/admin.py (1 hunks)
  • commcare_connect/solicitations/apps.py (1 hunks)
  • commcare_connect/solicitations/fixtures/sample_solicitations.yaml (1 hunks)
  • commcare_connect/solicitations/forms.py (1 hunks)
  • commcare_connect/solicitations/helpers.py (1 hunks)
  • commcare_connect/solicitations/instructions/Access-Security-Overview.md (1 hunks)
  • commcare_connect/solicitations/instructions/JJ_questions.md (1 hunks)
  • commcare_connect/solicitations/instructions/QA-prompt.md (1 hunks)
  • commcare_connect/solicitations/instructions/implement-plan.md (1 hunks)
  • commcare_connect/solicitations/instructions/labs.md (1 hunks)
  • commcare_connect/solicitations/instructions/rename-progress-summary.md (1 hunks)
  • commcare_connect/solicitations/instructions/review.MD (1 hunks)
  • commcare_connect/solicitations/instructions/table_review.md (1 hunks)
  • commcare_connect/solicitations/instructions/user-organization-program-relationships.md (1 hunks)
  • commcare_connect/solicitations/management/commands/load_solicitation_fixtures.py (1 hunks)
  • commcare_connect/solicitations/management/commands/load_uat_data.py (1 hunks)
  • commcare_connect/solicitations/management/commands/test_dashboard_links.py (1 hunks)
  • commcare_connect/solicitations/migrations/0001_initial.py (1 hunks)
  • commcare_connect/solicitations/migrations/0002_alter_solicitationresponse_status.py (1 hunks)
  • commcare_connect/solicitations/migrations/0003_alter_solicitationresponse_responses.py (1 hunks)
  • commcare_connect/solicitations/migrations/0004_add_response_attachment_model.py (1 hunks)
  • commcare_connect/solicitations/migrations/0005_remove_solicitationresponse_attachments_and_more.py (1 hunks)
  • commcare_connect/solicitations/migrations/0006_remove_eoi_rfp_relationships.py (1 hunks)
  • commcare_connect/solicitations/migrations/0007_remove_completed_status.py (1 hunks)
  • commcare_connect/solicitations/migrations/0008_simplify_response_status.py (1 hunks)
  • commcare_connect/solicitations/models.py (1 hunks)
  • commcare_connect/solicitations/tables.py (1 hunks)
  • commcare_connect/solicitations/tests/factories.py (1 hunks)
  • commcare_connect/solicitations/tests/test_authenticated_views.py (1 hunks)
  • commcare_connect/solicitations/tests/test_forms.py (1 hunks)
  • commcare_connect/solicitations/tests/test_helpers.py (1 hunks)
  • commcare_connect/solicitations/tests/test_models.py (1 hunks)
  • commcare_connect/solicitations/tests/test_permissions.py (1 hunks)
  • commcare_connect/solicitations/tests/test_views.py (1 hunks)
  • commcare_connect/solicitations/urls.py (1 hunks)
  • commcare_connect/solicitations/views.py (1 hunks)
  • commcare_connect/static/js/datetime-utils.js (1 hunks)
  • commcare_connect/templates/solicitations/components/file_attachments.html (1 hunks)
  • commcare_connect/templates/solicitations/components/organization_overview.html (1 hunks)
  • commcare_connect/templates/solicitations/components/response_answers.html (1 hunks)
  • commcare_connect/templates/solicitations/components/stats_card.html (1 hunks)
  • commcare_connect/templates/solicitations/components/table_section.html (1 hunks)
  • commcare_connect/templates/solicitations/draft_list.html (1 hunks)
  • commcare_connect/templates/solicitations/organization_required.html (1 hunks)
  • commcare_connect/templates/solicitations/public_base.html (1 hunks)
  • commcare_connect/templates/solicitations/response_detail.html (1 hunks)
  • commcare_connect/templates/solicitations/response_form.html (1 hunks)
  • commcare_connect/templates/solicitations/response_list.html (1 hunks)
  • commcare_connect/templates/solicitations/response_review.html (1 hunks)
  • commcare_connect/templates/solicitations/response_success.html (1 hunks)
  • commcare_connect/templates/solicitations/review_form.html (1 hunks)
  • commcare_connect/templates/solicitations/solicitation_detail.html (1 hunks)
  • commcare_connect/templates/solicitations/solicitation_form.html (1 hunks)
  • commcare_connect/templates/solicitations/solicitation_list.html (1 hunks)
  • commcare_connect/templates/solicitations/unified_dashboard.html (1 hunks)
  • config/settings/base.py (1 hunks)
  • config/urls.py (1 hunks)
  • demo_decorator.py (1 hunks)
  • requirements/base.txt (3 hunks)
  • requirements/production.txt (4 hunks)
  • tatus (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (27)
commcare_connect/solicitations/tests/test_helpers.py (4)
commcare_connect/solicitations/helpers.py (3)
  • calculate_response_permissions (30-88)
  • get_solicitation_dashboard_statistics (146-176)
  • process_solicitation_questions (179-214)
commcare_connect/solicitations/models.py (4)
  • Solicitation (10-74)
  • SolicitationResponse (106-149)
  • Status (19-22)
  • Status (111-113)
commcare_connect/solicitations/tests/factories.py (3)
  • SolicitationFactory (69-113)
  • SolicitationResponseFactory (146-160)
  • questions (167-188)
commcare_connect/conftest.py (2)
  • user (50-51)
  • organization (45-46)
commcare_connect/solicitations/tests/test_authenticated_views.py (6)
commcare_connect/program/tests/factories.py (1)
  • ProgramFactory (9-20)
commcare_connect/solicitations/models.py (8)
  • Solicitation (10-74)
  • SolicitationResponse (106-149)
  • SolicitationReview (182-205)
  • Type (15-17)
  • Type (82-87)
  • Status (19-22)
  • Status (111-113)
  • Recommendation (187-190)
commcare_connect/solicitations/tests/factories.py (7)
  • SolicitationFactory (69-113)
  • SolicitationResponseFactory (146-160)
  • SolicitationReviewFactory (225-242)
  • SolicitationWithQuestionsFactory (163-188)
  • title (86-89)
  • description (92-96)
  • questions (167-188)
commcare_connect/users/tests/factories.py (2)
  • OrganizationFactory (48-52)
  • UserFactory (10-26)
commcare_connect/conftest.py (2)
  • user (50-51)
  • organization (45-46)
commcare_connect/solicitations/forms.py (2)
  • save (150-173)
  • save (326-343)
commcare_connect/solicitations/migrations/0004_add_response_attachment_model.py (2)
commcare_connect/solicitations/migrations/0001_initial.py (1)
  • Migration (8-277)
commcare_connect/solicitations/migrations/0003_alter_solicitationresponse_responses.py (1)
  • Migration (6-17)
commcare_connect/solicitations/tests/test_views.py (2)
commcare_connect/solicitations/models.py (5)
  • Solicitation (10-74)
  • Status (19-22)
  • Status (111-113)
  • Type (15-17)
  • Type (82-87)
commcare_connect/solicitations/tests/factories.py (6)
  • EOIFactory (116-119)
  • RFPFactory (122-125)
  • SolicitationFactory (69-113)
  • title (86-89)
  • description (92-96)
  • scope_of_work (103-108)
commcare_connect/solicitations/migrations/0008_simplify_response_status.py (2)
commcare_connect/solicitations/models.py (1)
  • SolicitationResponse (106-149)
commcare_connect/solicitations/migrations/0007_remove_completed_status.py (1)
  • Migration (6-22)
commcare_connect/solicitations/tables.py (3)
commcare_connect/solicitations/models.py (9)
  • Solicitation (10-74)
  • SolicitationResponse (106-149)
  • Meta (61-67)
  • Meta (98-100)
  • Meta (124-130)
  • Meta (164-165)
  • Meta (200-202)
  • Status (19-22)
  • Status (111-113)
commcare_connect/utils/tables.py (1)
  • OrgContextTable (26-29)
commcare_connect/organization/models.py (1)
  • is_admin (48-49)
commcare_connect/solicitations/tests/test_models.py (3)
commcare_connect/solicitations/models.py (9)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • SolicitationResponse (106-149)
  • Status (19-22)
  • Status (111-113)
  • can_accept_responses (73-74)
  • is_draft (136-138)
  • is_submitted (141-143)
  • submit (145-149)
commcare_connect/solicitations/tests/factories.py (4)
  • SolicitationFactory (69-113)
  • SolicitationResponseFactory (146-160)
  • questions (167-188)
  • responses (195-222)
commcare_connect/conftest.py (2)
  • user (50-51)
  • organization (45-46)
commcare_connect/solicitations/migrations/0003_alter_solicitationresponse_responses.py (2)
commcare_connect/solicitations/migrations/0001_initial.py (1)
  • Migration (8-277)
commcare_connect/solicitations/migrations/0004_add_response_attachment_model.py (1)
  • Migration (8-50)
commcare_connect/solicitations/tests/factories.py (3)
commcare_connect/program/tests/factories.py (1)
  • ProgramFactory (9-20)
commcare_connect/solicitations/models.py (14)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • SolicitationResponse (106-149)
  • SolicitationReview (182-205)
  • Type (15-17)
  • Type (82-87)
  • Meta (61-67)
  • Meta (98-100)
  • Meta (124-130)
  • Meta (164-165)
  • Meta (200-202)
  • Status (19-22)
  • Status (111-113)
  • Recommendation (187-190)
commcare_connect/users/tests/factories.py (2)
  • OrganizationFactory (48-52)
  • UserFactory (10-26)
commcare_connect/solicitations/tests/test_forms.py (6)
commcare_connect/program/tests/factories.py (1)
  • ProgramFactory (9-20)
commcare_connect/solicitations/forms.py (4)
  • SolicitationForm (176-343)
  • SolicitationResponseForm (12-173)
  • save (150-173)
  • save (326-343)
commcare_connect/solicitations/models.py (6)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • Type (15-17)
  • Type (82-87)
  • Status (19-22)
  • Status (111-113)
commcare_connect/solicitations/tests/factories.py (6)
  • SolicitationWithQuestionsFactory (163-188)
  • title (86-89)
  • target_population (99-100)
  • scope_of_work (103-108)
  • questions (167-188)
  • responses (195-222)
commcare_connect/users/tests/factories.py (1)
  • UserFactory (10-26)
commcare_connect/conftest.py (2)
  • user (50-51)
  • organization (45-46)
commcare_connect/solicitations/management/commands/load_solicitation_fixtures.py (5)
commcare_connect/opportunity/models.py (1)
  • DeliveryType (53-59)
commcare_connect/organization/models.py (3)
  • Organization (11-25)
  • UserOrganizationMembership (28-62)
  • Role (29-32)
commcare_connect/program/models.py (1)
  • Program (9-26)
commcare_connect/solicitations/models.py (3)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • delete (175-179)
commcare_connect/users/models.py (1)
  • User (13-64)
commcare_connect/solicitations/models.py (5)
commcare_connect/organization/models.py (1)
  • Organization (11-25)
commcare_connect/program/models.py (1)
  • Program (9-26)
commcare_connect/users/models.py (1)
  • User (13-64)
commcare_connect/solicitations/tests/factories.py (9)
  • description (92-96)
  • target_population (99-100)
  • scope_of_work (103-108)
  • estimated_scale (111-113)
  • Meta (72-73)
  • Meta (129-130)
  • Meta (147-148)
  • Meta (228-229)
  • responses (195-222)
commcare_connect/solicitations/forms.py (4)
  • Meta (181-257)
  • Meta (351-353)
  • save (150-173)
  • save (326-343)
commcare_connect/solicitations/migrations/0007_remove_completed_status.py (1)
commcare_connect/solicitations/migrations/0008_simplify_response_status.py (1)
  • Migration (22-38)
commcare_connect/program/migrations/0002_alter_program_description.py (1)
commcare_connect/program/migrations/0001_initial.py (1)
  • Migration (7-99)
commcare_connect/opportunity/migrations/0083_alter_uservisit_location.py (1)
commcare_connect/opportunity/migrations/0030_uservisit_location.py (1)
  • Migration (6-17)
commcare_connect/solicitations/helpers.py (1)
commcare_connect/solicitations/models.py (9)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • SolicitationResponse (106-149)
  • can_accept_responses (73-74)
  • Status (19-22)
  • Status (111-113)
  • Type (15-17)
  • Type (82-87)
  • delete (175-179)
commcare_connect/reports/views.py (2)
commcare_connect/solicitations/views.py (4)
  • test_func (44-46)
  • test_func (57-63)
  • test_func (76-106)
  • test_func (1100-1115)
commcare_connect/program/views.py (1)
  • test_func (35-40)
commcare_connect/solicitations/urls.py (1)
commcare_connect/solicitations/views.py (13)
  • SolicitationCreateOrUpdate (296-431)
  • SolicitationDetailView (217-293)
  • SolicitationListView (171-214)
  • SolicitationResponseCreateOrUpdate (586-811)
  • SolicitationResponseDetailView (538-583)
  • SolicitationResponseDraftListView (814-828)
  • SolicitationResponseReviewCreateOrUpdate (834-968)
  • SolicitationResponseSuccessView (522-535)
  • SolicitationResponseTableView (438-519)
  • UnifiedSolicitationDashboard (1084-1415)
  • delete_attachment (1056-1076)
  • save_draft_ajax (973-998)
  • upload_attachment (1003-1051)
commcare_connect/solicitations/views.py (5)
commcare_connect/program/models.py (1)
  • Program (9-26)
commcare_connect/solicitations/forms.py (5)
  • SolicitationForm (176-343)
  • SolicitationResponseForm (12-173)
  • SolicitationReviewForm (346-414)
  • save (150-173)
  • save (326-343)
commcare_connect/solicitations/helpers.py (5)
  • build_question_context (217-246)
  • calculate_response_permissions (30-88)
  • get_solicitation_dashboard_statistics (146-176)
  • process_solicitation_questions (179-214)
  • update_solicitation_questions (249-288)
commcare_connect/solicitations/models.py (9)
  • ResponseAttachment (152-179)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • SolicitationResponse (106-149)
  • SolicitationReview (182-205)
  • Status (19-22)
  • Status (111-113)
  • can_accept_responses (73-74)
  • delete (175-179)
commcare_connect/solicitations/tables.py (3)
  • ProgramTable (64-125)
  • SolicitationResponseAndReviewTable (207-342)
  • SolicitationTable (131-204)
commcare_connect/solicitations/admin.py (1)
commcare_connect/solicitations/models.py (4)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • SolicitationResponse (106-149)
  • SolicitationReview (182-205)
commcare_connect/solicitations/tests/test_permissions.py (4)
commcare_connect/solicitations/views.py (6)
  • SolicitationAccessMixin (36-46)
  • SolicitationManagerMixin (49-63)
  • test_func (44-46)
  • test_func (57-63)
  • test_func (76-106)
  • test_func (1100-1115)
commcare_connect/users/tests/factories.py (1)
  • UserFactory (10-26)
commcare_connect/conftest.py (2)
  • user (50-51)
  • organization (45-46)
commcare_connect/organization/models.py (1)
  • Role (29-32)
commcare_connect/solicitations/migrations/0002_alter_solicitationresponse_status.py (2)
commcare_connect/solicitations/migrations/0001_initial.py (1)
  • Migration (8-277)
commcare_connect/solicitations/migrations/0003_alter_solicitationresponse_responses.py (1)
  • Migration (6-17)
commcare_connect/solicitations/migrations/0001_initial.py (3)
commcare_connect/solicitations/migrations/0002_alter_solicitationresponse_status.py (1)
  • Migration (6-28)
commcare_connect/solicitations/migrations/0003_alter_solicitationresponse_responses.py (1)
  • Migration (6-17)
commcare_connect/solicitations/migrations/0004_add_response_attachment_model.py (1)
  • Migration (8-50)
commcare_connect/program/models.py (1)
commcare_connect/solicitations/tests/factories.py (1)
  • description (92-96)
commcare_connect/solicitations/management/commands/load_uat_data.py (7)
commcare_connect/organization/models.py (3)
  • Organization (11-25)
  • UserOrganizationMembership (28-62)
  • Role (29-32)
commcare_connect/opportunity/models.py (1)
  • DeliveryType (53-59)
commcare_connect/program/models.py (1)
  • Program (9-26)
commcare_connect/solicitations/models.py (5)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • SolicitationResponse (106-149)
  • SolicitationReview (182-205)
  • delete (175-179)
commcare_connect/users/models.py (1)
  • User (13-64)
commcare_connect/solicitations/forms.py (2)
  • save (150-173)
  • save (326-343)
commcare_connect/solicitations/tests/factories.py (2)
  • responses (195-222)
  • questions (167-188)
commcare_connect/solicitations/management/commands/test_dashboard_links.py (2)
commcare_connect/users/models.py (1)
  • User (13-64)
commcare_connect/solicitations/management/commands/load_uat_data.py (2)
  • add_arguments (25-34)
  • handle (361-431)
commcare_connect/solicitations/forms.py (3)
commcare_connect/solicitations/helpers.py (1)
  • process_question_form_data (91-120)
commcare_connect/solicitations/models.py (10)
  • Solicitation (10-74)
  • SolicitationQuestion (77-103)
  • SolicitationResponse (106-149)
  • SolicitationReview (182-205)
  • Meta (61-67)
  • Meta (98-100)
  • Meta (124-130)
  • Meta (164-165)
  • Meta (200-202)
  • Recommendation (187-190)
commcare_connect/solicitations/tests/factories.py (9)
  • questions (167-188)
  • responses (195-222)
  • Meta (72-73)
  • Meta (129-130)
  • Meta (147-148)
  • Meta (228-229)
  • target_population (99-100)
  • scope_of_work (103-108)
  • estimated_scale (111-113)
🪛 GitHub Actions: CI
QUICK_START_GUIDE.md

[error] 1-1: Prettier formatting check failed. File was modified by the hook; run 'prettier --write' to fix code style issues.

commcare_connect/reports/views.py

[error] 36-36: flake8: undefined name 'LoginRequiredMixin' (F821) detected in views.py


[error] 50-50: flake8: undefined name 'UserPassesTestMixin' (F821) detected in views.py

🪛 LanguageTool
commcare_connect/solicitations/instructions/user-organization-program-relationships.md

[style] ~89-~89: To form a complete sentence, be sure to include a subject or ‘there’.
Context: ...nce accepted User Requirements: - Can be ADMIN, MEMBER, or VIEWER - Different...

(MISSING_IT_THERE)

commcare_connect/solicitations/instructions/table_review.md

[uncategorized] ~7-~7: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... names duplicate model definitions. ## HIGH VALUE Simplifications 🔥 ### 1. Remove Redun...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~152-~152: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...enefit:** More predictable behavior ## LOW VALUE Simplifications 🟢 ### 8. Extract Repe...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

QUICK_START_GUIDE.md

[style] ~12-~12: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...✅ ## 🏃‍♂️ Quick Start (Every Time You Want to Run the App) ### 1. Navigate to Projec...

(REP_WANT_TO_VB)

commcare_connect/solicitations/instructions/QA-prompt.md

[style] ~131-~131: Consider using a different verb for a more formal wording.
Context: ...and include a proposed cursor prompt to fix this issue

(FIX_RESOLVE)

🪛 markdownlint-cli2 (0.18.1)
commcare_connect/solicitations/instructions/labs.md

11-11: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

commcare_connect/solicitations/instructions/review.MD

70-70: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


79-79: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


103-103: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


116-116: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


125-125: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


145-145: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


169-169: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


181-181: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


198-198: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


207-207: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


223-223: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


234-234: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


243-243: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


260-260: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

commcare_connect/solicitations/instructions/user-organization-program-relationships.md

9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

QUICK_START_GUIDE.md

38-38: Bare URL used

(MD034, no-bare-urls)


52-52: Bare URL used

(MD034, no-bare-urls)


170-170: Bare URL used

(MD034, no-bare-urls)


171-171: Bare URL used

(MD034, no-bare-urls)


172-172: Bare URL used

(MD034, no-bare-urls)


173-173: Bare URL used

(MD034, no-bare-urls)


174-174: Bare URL used

(MD034, no-bare-urls)


210-210: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

commcare_connect/solicitations/instructions/QA-prompt.md

10-10: Bare URL used

(MD034, no-bare-urls)


16-16: Bare URL used

(MD034, no-bare-urls)

commcare_connect/solicitations/instructions/implement-plan.md

122-122: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


172-172: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


188-188: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


204-204: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


219-219: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


263-263: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


273-273: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


283-283: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🪛 Ruff (0.14.3)
commcare_connect/solicitations/tests/test_helpers.py

155-155: Unpacked variable error is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

commcare_connect/solicitations/migrations/0005_remove_solicitationresponse_attachments_and_more.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-86: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

demo_decorator.py

1-1: Shebang is present but file is not executable

(EXE001)

commcare_connect/solicitations/migrations/0004_add_response_attachment_model.py

9-12: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


14-50: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/tests/test_views.py

55-55: Unused method argument: solicitation_type

(ARG002)

commcare_connect/solicitations/migrations/0008_simplify_response_status.py

6-6: Unused function argument: schema_editor

(ARG001)


23-25: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


27-38: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/tables.py

47-47: Use of mark_safe may expose cross-site scripting vulnerabilities

(S308)


88-88: Unused method argument: record

(ARG002)


125-125: Use of mark_safe may expose cross-site scripting vulnerabilities

(S308)


204-204: Use of mark_safe may expose cross-site scripting vulnerabilities

(S308)


246-246: Unused method argument: record

(ARG002)


268-269: try-except-pass detected, consider logging the exception

(S110)


268-268: Do not catch blind exception: Exception

(BLE001)


278-279: try-except-pass detected, consider logging the exception

(S110)


278-278: Do not catch blind exception: Exception

(BLE001)


327-327: Use of mark_safe may expose cross-site scripting vulnerabilities

(S308)

commcare_connect/solicitations/migrations/0003_alter_solicitationresponse_responses.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-17: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/tests/factories.py

87-87: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


93-93: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


94-94: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


100-100: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


104-104: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


112-112: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


167-167: Unused method argument: kwargs

(ARG002)


177-177: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


178-178: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


195-195: Unused method argument: extracted

(ARG002)


195-195: Unused method argument: kwargs

(ARG002)


217-221: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)

commcare_connect/solicitations/management/commands/load_solicitation_fixtures.py

49-49: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


49-49: Avoid specifying long messages outside the exception class

(TRY003)


51-51: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


51-51: Avoid specifying long messages outside the exception class

(TRY003)


231-231: Unused method argument: args

(ARG002)

commcare_connect/solicitations/models.py

62-62: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


63-67: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


99-99: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


100-100: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


125-125: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


126-126: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


127-130: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


165-165: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


201-201: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


202-202: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/migrations/0007_remove_completed_status.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-22: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/program/migrations/0002_alter_program_description.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-17: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/opportunity/migrations/0083_alter_uservisit_location.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-17: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/helpers.py

209-209: Consider moving this statement to an else block

(TRY300)


212-212: Use explicit conversion flag

Replace with conversion flag

(RUF010)


213-213: Do not catch blind exception: Exception

(BLE001)


214-214: Use explicit conversion flag

Replace with conversion flag

(RUF010)


283-283: Consider moving this statement to an else block

(TRY300)


286-286: Use explicit conversion flag

Replace with conversion flag

(RUF010)


287-287: Do not catch blind exception: Exception

(BLE001)


288-288: Use explicit conversion flag

Replace with conversion flag

(RUF010)

commcare_connect/reports/views.py

36-36: Undefined name LoginRequiredMixin

(F821)


36-36: Undefined name UserPassesTestMixin

(F821)

commcare_connect/solicitations/views.py

157-157: Avoid specifying long messages outside the exception class

(TRY003)


309-309: Unused method argument: queryset

(ARG002)


599-599: Unused method argument: queryset

(ARG002)


614-614: Avoid specifying long messages outside the exception class

(TRY003)


618-618: Avoid specifying long messages outside the exception class

(TRY003)


809-811: try-except-pass detected, consider logging the exception

(S110)


809-809: Do not catch blind exception: Exception

(BLE001)


847-847: Unused method argument: queryset

(ARG002)


983-983: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


997-997: Do not catch blind exception: Exception

(BLE001)


1012-1012: Unpacked variable created is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


1048-1048: Do not catch blind exception: Exception

(BLE001)


1049-1049: Use explicit conversion flag

Replace with conversion flag

(RUF010)


1073-1073: Do not catch blind exception: Exception

(BLE001)


1074-1074: Use explicit conversion flag

Replace with conversion flag

(RUF010)

commcare_connect/solicitations/admin.py

8-8: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


9-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


10-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-11: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


25-25: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


26-26: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


27-27: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


28-28: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


37-37: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


38-38: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


39-39: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


40-40: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


52-52: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


53-53: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


54-54: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


55-55: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/migrations/0006_remove_eoi_rfp_relationships.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-35: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/tests/test_permissions.py

15-15: Unused method argument: request

(ARG002)


22-22: Unused method argument: request

(ARG002)

commcare_connect/solicitations/migrations/0002_alter_solicitationresponse_status.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-28: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/migrations/0001_initial.py

11-15: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


17-277: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

commcare_connect/solicitations/management/commands/load_uat_data.py

227-227: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


233-233: Loop control variable i not used within loop body

Rename unused i to _i

(B007)


249-249: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


277-277: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


279-279: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


330-330: Unused method argument: programs

(ARG002)


353-353: Unpacked variable membership is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)


361-361: Unused method argument: args

(ARG002)


390-390: Do not catch blind exception: Exception

(BLE001)

commcare_connect/solicitations/management/commands/test_dashboard_links.py

32-32: Unused method argument: args

(ARG002)


46-46: Consider [*current_allowed_hosts, "testserver"] instead of concatenation

Replace with [*current_allowed_hosts, "testserver"]

(RUF005)


63-63: Do not catch blind exception: Exception

(BLE001)


64-64: Use explicit conversion flag

Replace with conversion flag

(RUF010)

commcare_connect/solicitations/forms.py

183-192: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


193-257: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


319-319: Avoid specifying long messages outside the exception class

(TRY003)


322-322: Avoid specifying long messages outside the exception class

(TRY003)


353-353: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

Comment on lines +21 to +27
env:
AWS_REGION: us-east-1
ECS_CLUSTER: labs-jj-cluster
ECS_WEB_SERVICE: labs-jj-web
ECS_WORKER_SERVICE: labs-jj-worker
ECR_REPOSITORY: labs-jj-commcare-connect
ECR_REGISTRY: 858923557655.dkr.ecr.us-east-1.amazonaws.com
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move sensitive AWS identifiers to secrets and parameterize environment variables.

The workflow exposes hardcoded AWS account ID (858923557655) in the ECR registry and uses hardcoded user-specific resource names (labs-jj-*). This configuration is not suitable for a shared repository and creates a security/operational burden.

Recommended actions:

  1. Move AWS account ID and region to GitHub environment secrets or organization variables
  2. Create repository/environment-specific configuration (e.g., via GitHub environments feature)
  3. Allow different deployments per branch or environment

Example refactor:

 env:
-  AWS_REGION: us-east-1
-  ECS_CLUSTER: labs-jj-cluster
-  ECS_WEB_SERVICE: labs-jj-web
-  ECS_WORKER_SERVICE: labs-jj-worker
-  ECR_REPOSITORY: labs-jj-commcare-connect
-  ECR_REGISTRY: 858923557655.dkr.ecr.us-east-1.amazonaws.com
+  AWS_REGION: ${{ secrets.AWS_REGION }}
+  ECS_CLUSTER: ${{ secrets.ECS_CLUSTER }}
+  ECS_WEB_SERVICE: ${{ secrets.ECS_WEB_SERVICE }}
+  ECS_WORKER_SERVICE: ${{ secrets.ECS_WORKER_SERVICE }}
+  ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
+  ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}

Alternatively, use GitHub Environments for deployment-specific configs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
env:
AWS_REGION: us-east-1
ECS_CLUSTER: labs-jj-cluster
ECS_WEB_SERVICE: labs-jj-web
ECS_WORKER_SERVICE: labs-jj-worker
ECR_REPOSITORY: labs-jj-commcare-connect
ECR_REGISTRY: 858923557655.dkr.ecr.us-east-1.amazonaws.com
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
ECS_CLUSTER: ${{ secrets.ECS_CLUSTER }}
ECS_WEB_SERVICE: ${{ secrets.ECS_WEB_SERVICE }}
ECS_WORKER_SERVICE: ${{ secrets.ECS_WORKER_SERVICE }}
ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}
🤖 Prompt for AI Agents
.github/workflows/deploy-labs.yml lines 21-27: the workflow hardcodes sensitive
and environment-specific values (AWS account ID, region and lab-specific
resource names), so move those to secrets/repository or environment variables
and parameterize the resource names; replace the literal ECR_REGISTRY value with
a constructed value using a secret like AWS_ACCOUNT_ID and a secret/var for
AWS_REGION, move AWS_REGION into a secret/organization variable, and replace
ECS_CLUSTER/ECS_WEB_SERVICE/ECS_WORKER_SERVICE/ECR_REPOSITORY with inputs or
repo/environment variables (or GitHub Environment-specific variables) so
different branches/environments can supply different names and nobody stores
account IDs in source; update workflow to read the secrets/vars and construct
registry/service names at runtime and add documentation for required
secrets/environment configuration.

Comment on lines 66 to 106
- name: Run database migrations
if: ${{ github.event.inputs.run_migrations == 'true' }}
run: |
echo "🔄 Running database migrations..."
# Get the latest task definition
TASK_DEF_ARN=$(aws ecs describe-task-definition \
--task-definition ${{ env.ECS_WEB_SERVICE }} \
--query 'taskDefinition.taskDefinitionArn' \
--output text)
echo "Using task definition: $TASK_DEF_ARN"
# Run migration task
TASK_ARN=$(aws ecs run-task \
--cluster ${{ env.ECS_CLUSTER }} \
--task-definition $TASK_DEF_ARN \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-06646effb09be2f42],securityGroups=[sg-0666a5ed512c97d9d],assignPublicIp=ENABLED}" \
--overrides '{"containerOverrides":[{"name":"web","command":["/start_migrate"]}]}' \
--query 'tasks[0].taskArn' \
--output text)
echo "Migration task started: $TASK_ARN"
echo "Waiting for migration to complete..."
# Wait for task to complete
aws ecs wait tasks-stopped \
--cluster ${{ env.ECS_CLUSTER }} \
--tasks $TASK_ARN
# Check exit code
EXIT_CODE=$(aws ecs describe-tasks \
--cluster ${{ env.ECS_CLUSTER }} \
--tasks $TASK_ARN \
--query 'tasks[0].containers[0].exitCode' \
--output text)
if [ "$EXIT_CODE" == "0" ]; then
echo "✅ Migrations completed successfully"
else
echo "❌ Migrations failed with exit code: $EXIT_CODE"
echo "Check CloudWatch Logs: https://console.aws.amazon.com/cloudwatch/home?region=${{ env.AWS_REGION }}#logsV2:log-groups/log-group/%2Fecs%2F${{ env.ECS_WEB_SERVICE }}"
exit 1
fi
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Parameterize network configuration and add robustness checks for migration task.

The migration step has several concerns:

  1. Hardcoded network configuration (line 84): Subnet and security group IDs are hardcoded, making the workflow inflexible and exposing infrastructure details.
  2. No timeout on wait operation: The aws ecs wait tasks-stopped command has no explicit timeout; it could hang indefinitely if the task doesn't stop.
  3. Unvalidated container command: The /start_migrate command is assumed to exist; verify it's present in the Dockerfile/container definition.

Recommended improvements:

       - name: Run database migrations
         if: ${{ github.event.inputs.run_migrations == 'true' }}
         run: |
           echo "🔄 Running database migrations..."

           # Get the latest task definition
           TASK_DEF_ARN=$(aws ecs describe-task-definition \
             --task-definition ${{ env.ECS_WEB_SERVICE }} \
             --query 'taskDefinition.taskDefinitionArn' \
             --output text)

           echo "Using task definition: $TASK_DEF_ARN"

           # Run migration task
           TASK_ARN=$(aws ecs run-task \
             --cluster ${{ env.ECS_CLUSTER }} \
             --task-definition $TASK_DEF_ARN \
             --launch-type FARGATE \
             --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNET_IDS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" \
             --overrides '{"containerOverrides":[{"name":"web","command":["/start_migrate"]}]}' \
             --query 'tasks[0].taskArn' \
             --output text)

           echo "Migration task started: $TASK_ARN"
           echo "Waiting for migration to complete (timeout: 30 minutes)..."

           # Wait for task to complete with timeout
           timeout 1800 aws ecs wait tasks-stopped \
             --cluster ${{ env.ECS_CLUSTER }} \
             --tasks $TASK_ARN || { echo "❌ Migration task timed out or failed"; exit 1; }

Verify that /start_migrate is a valid entrypoint command in your container.


I'll verify the concerns raised in this review comment by examining the workflow file and related infrastructure code.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find and examine the deploy-labs.yml workflow file
fd -t f "deploy-labs.yml" --exec cat {} \;

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for Dockerfile and container definition files
fd -t f -e Dockerfile -o -name "*.dockerfile" | head -20

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Search for references to /start_migrate command in the codebase
rg -n "start_migrate" -C 3

Length of output: 223


🏁 Script executed:

#!/bin/bash
# Search for AWS network configuration patterns and how subnets/security groups are typically configured
rg -n "subnet-|sg-0" -C 2 | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for ECS task definition files or configurations
fd -t f -e json -o -e yaml -o -e yml | xargs grep -l "taskDefinition\|ecs" 2>/dev/null | head -20

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Check for any existing timeout patterns in GitHub workflows
rg -n "timeout" .github/workflows/ -C 2

Length of output: 0


I need to search more thoroughly for the workflow file and environment configuration. Let me refine my approach:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all workflow files, not just deploy-labs.yml
find .github/workflows -type f -name "*.yml" -o -name "*.yaml" 2>/dev/null

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Search for the deploy-labs workflow with broader patterns
rg -l "deploy-labs" --type yaml

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for environment variable definitions related to ECS or subnet
rg "ECS_SUBNET|ECS_SECURITY|subnet-|sg-" --type yaml -C 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for environment or secrets definitions in workflow files
rg "env:|secrets\." .github/workflows/ --type yaml -C 1 | head -60

Length of output: 1549


🏁 Script executed:

#!/bin/bash
# Check if there's a .github/workflows directory and list contents
ls -la .github/workflows/ 2>/dev/null || echo "Directory not found"

Length of output: 292


Now let me examine the actual deploy-labs.yml file to verify the concerns:


🏁 Script executed:

#!/bin/bash
# Extract and display the deploy-labs.yml file content
cat .github/workflows/deploy-labs.yml

Length of output: 6722


🏁 Script executed:

#!/bin/bash
# Search specifically for the hardcoded subnet and security group IDs
rg "subnet-06646effb09be2f42|sg-0666a5ed512c97d9d" .github/workflows/deploy-labs.yml

Length of output: 157


🏁 Script executed:

#!/bin/bash
# Search for all references to network configuration in the workflow
rg "awsvpcConfiguration|subnets|securityGroups" .github/workflows/deploy-labs.yml -C 3

Length of output: 451


🏁 Script executed:

#!/bin/bash
# Check for timeout command in deploy-labs.yml
rg "timeout|tasks-stopped" .github/workflows/deploy-labs.yml -C 2

Length of output: 154


🏁 Script executed:

#!/bin/bash
# Search for ECS environment variables across all workflow files
rg "ECS_CLUSTER|ECS_WEB_SERVICE" .github/workflows/ -C 1

Length of output: 3908


Now let me verify if the suggested secrets approach is configured in the repository:


🏁 Script executed:

#!/bin/bash
# Search for secrets configuration or documentation
rg "ECS_SUBNET|ECS_SECURITY" . --type md --type yaml --type json -C 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's any secrets documentation or config
find . -name "*.md" -o -name "secrets*" -o -name ".env*" 2>/dev/null | head -20

Length of output: 795


🏁 Script executed:

#!/bin/bash
# Look for any reference to subnet or security group IDs in documentation
rg "subnet|security.group" . --type md -C 2

Length of output: 0


Parameterize network configuration and add robustness checks for migration task.

The migration step requires two critical fixes:

  1. Hardcoded network configuration (line 84): Subnet and security group IDs are hardcoded, making the workflow inflexible and exposing infrastructure details. Move these to environment variables or secrets.
  2. No timeout on wait operation: The aws ecs wait tasks-stopped command (line 90) has no explicit timeout; it could hang indefinitely if the task doesn't stop.

Recommended improvements:

       - name: Run database migrations
         if: ${{ github.event.inputs.run_migrations == 'true' }}
         run: |
           echo "🔄 Running database migrations..."

           # Get the latest task definition
           TASK_DEF_ARN=$(aws ecs describe-task-definition \
             --task-definition ${{ env.ECS_WEB_SERVICE }} \
             --query 'taskDefinition.taskDefinitionArn' \
             --output text)

           echo "Using task definition: $TASK_DEF_ARN"

           # Run migration task
           TASK_ARN=$(aws ecs run-task \
             --cluster ${{ env.ECS_CLUSTER }} \
             --task-definition $TASK_DEF_ARN \
             --launch-type FARGATE \
             --network-configuration "awsvpcConfiguration={subnets=[${{ secrets.ECS_SUBNET_IDS }}],securityGroups=[${{ secrets.ECS_SECURITY_GROUPS }}],assignPublicIp=ENABLED}" \
             --overrides '{"containerOverrides":[{"name":"web","command":["/start_migrate"]}]}' \
             --query 'tasks[0].taskArn' \
             --output text)

           echo "Migration task started: $TASK_ARN"
           echo "Waiting for migration to complete (timeout: 30 minutes)..."

           # Wait for task to complete with timeout
           timeout 1800 aws ecs wait tasks-stopped \
             --cluster ${{ env.ECS_CLUSTER }} \
             --tasks $TASK_ARN || { echo "❌ Migration task timed out or failed"; exit 1; }
🤖 Prompt for AI Agents
In .github/workflows/deploy-labs.yml around lines 66 to 110, the migration step
hardcodes subnet and security group IDs and uses aws ecs wait without a timeout;
update the step to read subnet and security group IDs (and assignPublicIp if
needed) from environment variables or secrets (e.g., ${{ env.MIGRATION_SUBNET }}
and ${{ env.MIGRATION_SECURITY_GROUP }}) and validate they are set before
invoking run-task, and replace the single aws ecs wait call with a robust
polling/timeout approach (or implement a loop that checks task status via
describe-tasks with a maximum timeout/retries and fails if exceeded), also add
checks that TASK_ARN and EXIT_CODE are present and fail fast with clear errors
if run-task or describe-tasks return empty values.

Comment on lines 138 to 159
- name: Get public IP
id: get-ip
run: |
TASK_ARN=$(aws ecs list-tasks \
--cluster ${{ env.ECS_CLUSTER }} \
--service-name ${{ env.ECS_WEB_SERVICE }} \
--query 'taskArns[0]' \
--output text)
ENI_ID=$(aws ecs describe-tasks \
--cluster ${{ env.ECS_CLUSTER }} \
--tasks $TASK_ARN \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
--output text)
PUBLIC_IP=$(aws ec2 describe-network-interfaces \
--network-interface-ids $ENI_ID \
--query 'NetworkInterfaces[0].Association.PublicIp' \
--output text)
echo "public_ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
echo "Public IP: $PUBLIC_IP"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Harden ENI lookup query for robustness.

The public IP extraction assumes a specific structure for ECS task network attachments. If the attachment structure differs, the query will fail silently or return an empty string, leading to broken deployment summary URLs.

Add defensive checks:

       - name: Get public IP
         id: get-ip
         run: |
           TASK_ARN=$(aws ecs list-tasks \
             --cluster ${{ env.ECS_CLUSTER }} \
             --service-name ${{ env.ECS_WEB_SERVICE }} \
             --query 'taskArns[0]' \
             --output text)

+          if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" == "None" ]; then
+            echo "❌ Failed to find running task for service ${{ env.ECS_WEB_SERVICE }}"
+            exit 1
+          fi

           ENI_ID=$(aws ecs describe-tasks \
             --cluster ${{ env.ECS_CLUSTER }} \
             --tasks $TASK_ARN \
             --query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
             --output text)

+          if [ -z "$ENI_ID" ] || [ "$ENI_ID" == "None" ]; then
+            echo "❌ Failed to extract network interface ID from task"
+            exit 1
+          fi

           PUBLIC_IP=$(aws ec2 describe-network-interfaces \
             --network-interface-ids $ENI_ID \
             --query 'NetworkInterfaces[0].Association.PublicIp' \
             --output text)

+          if [ -z "$PUBLIC_IP" ] || [ "$PUBLIC_IP" == "None" ]; then
+            echo "⚠️  Warning: Could not retrieve public IP (might still be assigning)"
+            PUBLIC_IP="<pending>"
+          fi

           echo "public_ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
           echo "Public IP: $PUBLIC_IP"

I'll verify the current state of the workflow file and validate the review comment's concerns.


🏁 Script executed:

#!/bin/bash
# Check the current state of the deploy-labs.yml file around lines 138-159
cat .github/workflows/deploy-labs.yml | sed -n '130,170p' | cat -n

Length of output: 1951


🏁 Script executed:

#!/bin/bash
# Search for any existing error handling or defensive checks in the file
rg -n "TASK_ARN|ENI_ID|PUBLIC_IP" .github/workflows/deploy-labs.yml -A2 -B2

Length of output: 1593


🏁 Script executed:

#!/bin/bash
# Check how public_ip output is used in the workflow
rg -n "public_ip|get-ip" .github/workflows/deploy-labs.yml -A3 -B3

Length of output: 949


🏁 Script executed:

#!/bin/bash
# Verify the file exists and show full section for context
test -f .github/workflows/deploy-labs.yml && echo "File exists" || echo "File not found"
wc -l .github/workflows/deploy-labs.yml

Length of output: 49


Add defensive checks for AWS CLI command outputs to prevent silent failures.

The workflow lacks validation after each AWS CLI call in the "Get public IP" step. If TASK_ARN, ENI_ID, or PUBLIC_IP are empty or return "None", the step silently succeeds but produces broken deployment summary URLs (lines 167-168).

Add validation after each extraction:

      - name: Get public IP
        id: get-ip
        run: |
          TASK_ARN=$(aws ecs list-tasks \
            --cluster ${{ env.ECS_CLUSTER }} \
            --service-name ${{ env.ECS_WEB_SERVICE }} \
            --query 'taskArns[0]' \
            --output text)

+         if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" == "None" ]; then
+           echo "❌ Failed to find running task for service ${{ env.ECS_WEB_SERVICE }}"
+           exit 1
+         fi

          ENI_ID=$(aws ecs describe-tasks \
            --cluster ${{ env.ECS_CLUSTER }} \
            --tasks $TASK_ARN \
            --query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
            --output text)

+         if [ -z "$ENI_ID" ] || [ "$ENI_ID" == "None" ]; then
+           echo "❌ Failed to extract network interface ID from task"
+           exit 1
+         fi

          PUBLIC_IP=$(aws ec2 describe-network-interfaces \
            --network-interface-ids $ENI_ID \
            --query 'NetworkInterfaces[0].Association.PublicIp' \
            --output text)

+         if [ -z "$PUBLIC_IP" ] || [ "$PUBLIC_IP" == "None" ]; then
+           echo "⚠️  Warning: Could not retrieve public IP (might still be assigning)"
+           PUBLIC_IP="<pending>"
+         fi

          echo "public_ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
          echo "Public IP: $PUBLIC_IP"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Get public IP
id: get-ip
run: |
TASK_ARN=$(aws ecs list-tasks \
--cluster ${{ env.ECS_CLUSTER }} \
--service-name ${{ env.ECS_WEB_SERVICE }} \
--query 'taskArns[0]' \
--output text)
ENI_ID=$(aws ecs describe-tasks \
--cluster ${{ env.ECS_CLUSTER }} \
--tasks $TASK_ARN \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
--output text)
PUBLIC_IP=$(aws ec2 describe-network-interfaces \
--network-interface-ids $ENI_ID \
--query 'NetworkInterfaces[0].Association.PublicIp' \
--output text)
echo "public_ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
echo "Public IP: $PUBLIC_IP"
- name: Get public IP
id: get-ip
run: |
TASK_ARN=$(aws ecs list-tasks \
--cluster ${{ env.ECS_CLUSTER }} \
--service-name ${{ env.ECS_WEB_SERVICE }} \
--query 'taskArns[0]' \
--output text)
if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" == "None" ]; then
echo "❌ Failed to find running task for service ${{ env.ECS_WEB_SERVICE }}"
exit 1
fi
ENI_ID=$(aws ecs describe-tasks \
--cluster ${{ env.ECS_CLUSTER }} \
--tasks $TASK_ARN \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
--output text)
if [ -z "$ENI_ID" ] || [ "$ENI_ID" == "None" ]; then
echo "❌ Failed to extract network interface ID from task"
exit 1
fi
PUBLIC_IP=$(aws ec2 describe-network-interfaces \
--network-interface-ids $ENI_ID \
--query 'NetworkInterfaces[0].Association.PublicIp' \
--output text)
if [ -z "$PUBLIC_IP" ] || [ "$PUBLIC_IP" == "None" ]; then
echo "⚠️ Warning: Could not retrieve public IP (might still be assigning)"
PUBLIC_IP="<pending>"
fi
echo "public_ip=$PUBLIC_IP" >> $GITHUB_OUTPUT
echo "Public IP: $PUBLIC_IP"
🤖 Prompt for AI Agents
In .github/workflows/deploy-labs.yml around lines 138 to 159, add defensive
checks after each AWS CLI extraction (TASK_ARN, ENI_ID, PUBLIC_IP) to avoid
silent failures: after assigning TASK_ARN verify it is non-empty and not "None"
and if it is, emit an error (echo "::error::...") and exit 1; repeat the same
check for ENI_ID and for PUBLIC_IP, echo a descriptive error and exit 1 on
failure; keep the existing outputs when valid (echo "public_ip=... >>
$GITHUB_OUTPUT") so the workflow fails fast with a clear message instead of
producing broken URLs.

Comment on lines +11 to +17
operations = [
migrations.AlterField(
model_name="uservisit",
name="location",
field=models.CharField(max_length=255, null=True),
),
]
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify if this migration is necessary.

This migration alters the location field to CharField(max_length=255, null=True), which appears to be identical to the definition already added in migration 0030 (from 2024-01-31). Unless there were intermediate migrations between 0030 and 0083 that modified this field, this migration may be redundant.

Run the following script to check for any intermediate migrations that modified the location field:


🏁 Script executed:

#!/bin/bash
# Description: Search for migrations between 0030 and 0083 that reference the location field

# Find all migration files in the opportunity app
fd -e py . commcare_connect/opportunity/migrations/ --exec grep -l "location" {} \; | sort

# Look for the specific migrations to understand the history
rg -A 5 -B 2 "location" commcare_connect/opportunity/migrations/003[0-9]*.py commcare_connect/opportunity/migrations/004*.py commcare_connect/opportunity/migrations/005*.py commcare_connect/opportunity/migrations/006*.py commcare_connect/opportunity/migrations/007*.py commcare_connect/opportunity/migrations/008[0-2]*.py

Length of output: 3023



---

🏁 Script executed:

```shell
#!/bin/bash
# Display the full content of migration 0083 to check dependencies
cat commcare_connect/opportunity/migrations/0083_alter_uservisit_location.py

Length of output: 444



---

🏁 Script executed:

```shell
#!/bin/bash
# Find and display the UserVisit model definition to verify the current location field
rg -A 30 "class UserVisit" commcare_connect/opportunity/ --type py -g "*.py" | head -100

Length of output: 8357


Remove migration 0083—it is redundant and does not make any changes.

Migration 0030 initially set location to CharField(max_length=255, null=True). Migration 0083 attempts to alter the same field to the identical definition. No intermediate migrations between 0030 and 0083 modify the location field. This migration should be removed from the codebase or squashed into a preceding migration.

🧰 Tools
🪛 Ruff (0.14.3)

11-17: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🤖 Prompt for AI Agents
In commcare_connect/opportunity/migrations/0083_alter_uservisit_location.py
around lines 11 to 17, the migration alters UserVisit.location to
CharField(max_length=255, null=True) but this is identical to migration 0030 and
thus redundant; remove migration 0083 from the codebase (or squash it into an
earlier migration) so the duplicate change is not applied—delete the file and
any references to it, run makemigrations/checks to ensure no new migration is
generated, and run tests/migrations locally to confirm the migration history
remains consistent.

("name", models.CharField(max_length=255)),
("slug", models.SlugField(max_length=255, unique=True)),
("description", models.CharField()),
("description", models.CharField(max_length=255)),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify migration sequencing for description field.

The description field is defined with max_length=255 in this initial migration, and then a subsequent migration 0002_alter_program_description.py alters the same field to add the same constraint. This pattern suggests the initial migration may have been modified after the second migration was created.

Verify the migration history to ensure there are no conflicts:


🏁 Script executed:

#!/bin/bash
# Check if there are any conflicting migrations or if this was retroactively modified
echo "Checking migration dependencies and field history..."

# Look for any Program model definition in non-migration files
rg -n "class Program" --type=py -g '!*/migrations/*'

# Check for any other migrations that reference Program.description
rg -n "description" commcare_connect/program/migrations/ -A 2 -B 2

Length of output: 2820


I need to examine the full migration files and the current Program model to verify the migration sequencing. Let me gather more complete information.


🏁 Script executed:

#!/bin/bash
# Get the full content of migrations 0001 and 0002
echo "=== Migration 0001_initial.py ==="
cat commcare_connect/program/migrations/0001_initial.py

echo -e "\n=== Migration 0002_alter_program_description.py ==="
cat commcare_connect/program/migrations/0002_alter_program_description.py

echo -e "\n=== Current Program model ==="
rg -A 30 "^class Program\(BaseModel\)" commcare_connect/program/models.py

Length of output: 5656


I need to check the git history to determine if migration 0001 was retroactively modified after 0002 was created.


🏁 Script executed:

#!/bin/bash
# Check git history for the migration files
echo "=== Git log for 0001_initial.py ==="
git log --oneline --all -- commcare_connect/program/migrations/0001_initial.py

echo -e "\n=== Git log for 0002_alter_program_description.py ==="
git log --oneline --all -- commcare_connect/program/migrations/0002_alter_program_description.py

echo -e "\n=== Check if there are migrations after 0002 ==="
ls -la commcare_connect/program/migrations/ | grep "^-.*\.py$" | grep -E "000[3-9]|00[1-9][0-9]"

echo -e "\n=== All migration files in order ==="
ls -1 commcare_connect/program/migrations/*.py | sort

Length of output: 1301


I found a critical issue beyond the original concern. Let me examine the conflicting 0002 migration:


🏁 Script executed:

#!/bin/bash
echo "=== Migration 0002_update_delivery_type.py ==="
cat commcare_connect/program/migrations/0002_update_delivery_type.py

echo -e "\n=== Checking migration dependencies ==="
rg "dependencies\s*=" commcare_connect/program/migrations/000[12]*.py -A 3

Length of output: 1797


Critical migration sequencing conflict detected.

The repository contains two migrations both numbered 0002_*:

  • 0002_alter_program_description.py (alters description field to identical CharField(max_length=255))
  • 0002_update_delivery_type.py (data migration for delivery_type)

This violates Django's migration sequencing requirements—each migration number must be unique per app. Django will fail during migration application due to the ambiguous 0002 reference.

Required fixes:

  1. Remove 0002_alter_program_description.py (it's a no-op: the description field already has max_length=255 in the initial migration)
  2. Rename 0002_update_delivery_type.py to 0003_update_delivery_type.py and update its dependency from ("program", "0001_initial") to ("program", "0002_alter_program_description") OR to the next valid migration if 0002_alter is removed
🤖 Prompt for AI Agents
commcare_connect/program/migrations/0001_initial.py around line 26: there is a
migration sequencing conflict because a redundant
0002_alter_program_description.py (no-op) collides with
0002_update_delivery_type.py; remove
commcare_connect/program/migrations/0002_alter_program_description.py entirely,
keep only commcare_connect/program/migrations/0002_update_delivery_type.py as
the sole 0002 migration, and open 0002_update_delivery_type.py to ensure its
Migration.dependencies point to ("program", "0001_initial") (update the tuple if
needed) so migration numbers are unique and dependency chain remains valid.

Comment on lines +186 to +190
<a href="mailto:{{ solicitation.program.organization.name|slugify }}@commcareconnect.org"
class="button button-md outline-style">
<i class="fa-solid fa-envelope mr-2"></i>
Get in Touch
</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify email address construction logic.

The email address is constructed by slugifying the organization name and appending @commcareconnect.org. This approach could lead to issues:

  • Slugified names may not match actual email addresses
  • No validation that the constructed email exists
  • Organizations might have different contact email formats

Consider storing the contact email in the Program or Organization model instead of constructing it dynamically.

To verify if organization email addresses are stored elsewhere, run:


🏁 Script executed:

#!/bin/bash
# Search for email-related fields in Organization and Program models
ast-grep --pattern 'class Organization($$$) {
  $$$
  email = $$$
  $$$
}'

ast-grep --pattern 'class Program($$$) {
  $$$
  email = $$$
  $$$
}'

# Also search for any contact_email or similar fields
rg -n --type=py -C3 'contact.*email|email.*contact' commcare_connect/organization/models.py commcare_connect/program/models.py

Length of output: 0


I need to search the codebase with corrected queries. The initial script used incorrect syntax for Python classes. Let me investigate properly:


🏁 Script executed:

#!/bin/bash
# Find the actual model files
echo "=== Finding Organization and Program model files ==="
fd -e py -p '(organization|program).*models' commcare_connect/

echo -e "\n=== Searching for email-related fields in Python files ==="
rg -n --type=py 'email|contact.*email' commcare_connect/organization/ commcare_connect/program/ 2>/dev/null | head -30

echo -e "\n=== Checking for any email field definitions ==="
rg -n --type=py -B2 -A2 '^\s*(email|contact_email|contact_phone|phone)\s*=' commcare_connect/organization/models.py commcare_connect/program/models.py

echo -e "\n=== Searching for slugify usage in templates ==="
rg -n 'slugify' commcare_connect/templates/ -A2 -B2

echo -e "\n=== Checking for mailto or email construction patterns ==="
rg -n 'mailto:' commcare_connect/templates/ -B1 -A1

Length of output: 4518


Add contact_email field to Organization or Program model.

The email address is constructed by slugifying the organization name without validation. Search confirms no email or contact_email fields exist in Organization or Program models—only user-related emails. This means:

  • Slugified names won't reliably match intended email addresses
  • No validation that constructed emails exist
  • No support for custom contact email formats

Store a proper contact_email field on the model to replace the dynamic construction.

🤖 Prompt for AI Agents
In commcare_connect/templates/solicitations/solicitation_detail.html around
lines 186-190, the template builds a contact email by slugifying the
organization name which is brittle and unvalidated; add a proper contact_email
field to the persistent model (Organization or Program) and use it in the
template. Modify the chosen model to include a Django EmailField (contact_email)
with null=True, blank=True, and validators; create and run a migration; update
admin/forms/fixtures to expose and populate the field (or provide a sensible
default/fallback), add unit tests to validate formatting and presence where
required, and change the template to use program.contact_email or
organization.contact_email (with a safe fallback if empty) instead of
constructing an address from the slugified name.

Comment on lines +226 to +237
// Form submission handler
const solicitationForm = document.getElementById('solicitation-form');
const self = this; // Capture the correct 'this' context
if (solicitationForm) {
solicitationForm.addEventListener('submit', (e) => {
const questionsDataField = document.getElementById('questions-data');
if (questionsDataField) {
questionsDataField.value = JSON.stringify(self.questions);
console.log('Questions data being submitted:', self.questions);
}
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore hidden questions payload before submit

solicitationQuestionBuilder serializes the question list into an element with id questions-data, but the template never renders that hidden input. When the user saves, questionsDataField is null, so no JSON ever posts back and any edited/added questions are silently dropped on the server. Please add the hidden field (or update the selector) so the form actually submits the serialized questions.

@@
-        {% csrf_token %}
-
-        {% crispy form %}
+        {% csrf_token %}
+
+        <input type="hidden" id="questions-data" name="questions_data" value="[]">
+
+        {% crispy form %}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In commcare_connect/templates/solicitations/solicitation_form.html around lines
226 to 237, the script expects a hidden input with id "questions-data" but the
template does not render it so questions are never posted; add a hidden input
inside the solicitation form (e.g. <input type="hidden" id="questions-data"
name="questions_data">) ensuring the name matches the server-side field
expected, place it within the <form> before this script runs (or update the
selector in the script to match the existing input id/name if one exists), and
confirm the submit handler sets its value to JSON.stringify(this.questions) so
edited/added questions are submitted.

Comment on lines +15 to +17
```bash
cd /Users/sarveshtewari/commcare-connect
```
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove user-specific hardcoded path.

The guide contains a hardcoded path specific to one developer's machine (/Users/sarveshtewari/commcare-connect). This will not work for other developers and should be replaced with a generic placeholder.

Apply this diff:

 ### 1. Navigate to Project Directory
 ```bash
-cd /Users/sarveshtewari/commcare-connect
+cd /path/to/commcare-connect

<details>
<summary>🤖 Prompt for AI Agents</summary>

In QUICK_START_GUIDE.md around lines 15 to 17, the guide uses a hardcoded,
user-specific path (/Users/sarveshtewari/commcare-connect); replace it with a
generic placeholder such as /path/to/commcare-connect (or ./commcare-connect) so
instructions work for all developers and update the line accordingly.


</details>

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +46 to +48
```bash
cd /Users/sarveshtewari/commcare-connect
source .venv/bin/activate
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove duplicate user-specific hardcoded path.

Same issue as above—this hardcoded path should use a generic placeholder.

Apply this diff:

 ```bash
-cd /Users/sarveshtewari/commcare-connect
+cd /path/to/commcare-connect
 source .venv/bin/activate
 python manage.py runserver
🤖 Prompt for AI Agents
In QUICK_START_GUIDE.md around lines 46 to 48, there is a user-specific
hardcoded path "cd /Users/sarveshtewari/commcare-connect"; replace that exact
line with a generic placeholder such as "cd /path/to/commcare-connect" so the
guide is portable for other users, keeping the rest of the snippet (activate
virtualenv and runserver) unchanged.

tatus Outdated
Comment on lines 1 to 2
jj/EOI-pages
* main
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove this accidental Git output file.

This file appears to be terminal output from a Git command (possibly git branch or git status) that was accidentally committed. It contains ANSI color escape sequences and branch names, which serve no purpose in the repository.

Remove this file from the repository:

#!/bin/bash
# Remove the accidental git output file
git rm tatus
🤖 Prompt for AI Agents
In file tatus lines 1-2, the file is accidental Git command output containing
ANSI escape sequences and branch names; remove it from the repository by
deleting the file and committing that deletion (e.g., run git rm tatus, commit
with a clear message like "remove accidental git output file tatus", and push
the branch); if the file must be purged from history as well, run a
history-rewrite tool (git filter-branch or BFG) and coordinate with the team
before force-pushing.

- Enforce LF line endings in .gitattributes (eol=lf)
- Simplify database backups section in LABS-IMPLEMENTATION.md
- Complete session-based OAuth flow (no DB storage)
- LabsUser model compatible with Django User interface
- Custom middleware for authentication and URL whitelisting
- Login/logout/status pages with clean UI
- Updated user helpers/middleware to support LabsUser
- Added labs navigation from audit list page
Workers were being killed by OOM. With 1GB memory, 2 workers is appropriate.
- Remove 'Get public IP' step from workflow
- Update deployment summary to show labs.connect.dimagi.com URL
- Update architecture diagram to show ALB
- Update Key Design Decisions to reflect ALB usage
- Remove 'Get Current Public IP' troubleshooting section
- Remove intermediate progress messages (redundant with step names)
- Keep only essential debugging info (task ARN) and success/failure messages
- Retain deployment summary at end with URLs and links
GitHub Actions UI already shows workflow success/failure and all the info is available in the Actions console.
Copy link
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: 37

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
commcare_connect/contrib/sites/migrations/0003_set_site_domain_and_name.py (1)

19-37: Remove unreachable dead code.

The outer condition at line 19 already excludes SQLite (connection.vendor != "sqlite"), making the inner SQLite check at lines 29-30 unreachable and redundant. When the outer condition is met, connection.vendor is guaranteed to NOT be "sqlite", so the inner condition if connection.vendor == "sqlite": can never evaluate to true.

Apply this diff to remove the dead code:

     if created and connection.vendor != "sqlite":
         # We provided the ID explicitly when creating the Site entry, therefore the DB
         # sequence to auto-generate them wasn't used and is now out of sync. If we
         # don't do anything, we'll get a unique constraint violation the next time a
         # site is created.
         # To avoid this, we need to manually update DB sequence and make sure it's
         # greater than the maximum value.
         max_id = site_model.objects.order_by('-id').first().id
         with connection.cursor() as cursor:
-            # Skip sequence adjustment on SQLite where sequences are not used
-            if connection.vendor == "sqlite":
-                return
             cursor.execute("SELECT last_value from django_site_id_seq")
             (current_id,) = cursor.fetchone()
             if current_id <= max_id:
                 cursor.execute(
                     "alter sequence django_site_id_seq restart with %s",
                     [max_id + 1],
                 )
♻️ Duplicate comments (16)
commcare_connect/reports/views.py (1)

36-38: Add missing imports for mixin base classes.

The SuperUserRequiredMixin references LoginRequiredMixin and UserPassesTestMixin, but these classes are not imported, causing CI pipeline failures.

Add the required imports at the top of the file:

 from datetime import date, datetime, timedelta
 
 import django_filters
 import django_tables2 as tables
 from crispy_forms.helper import FormHelper
 from crispy_forms.layout import Column, Layout, Row
 from django import forms
 from django.conf import settings
+from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
 from django.db.models import Count, Sum
 from django.db.models.functions import TruncDate
 from django.http import JsonResponse
QUICK_START_GUIDE.md (2)

15-17: The hardcoded path /Users/sarveshtewari/commcare-connect should be replaced with a generic placeholder like /path/to/commcare-connect as previously noted.


46-48: Same hardcoded path issue as above - use a generic placeholder instead of the user-specific path.

.github/workflows/deploy-labs.yml (2)

21-27: The hardcoded AWS account ID and user-specific resource names should be moved to secrets/environment variables as previously noted. This affects portability and security.


67-106: The migration step requires parameterization of hardcoded network configuration (subnet and security group IDs) and a timeout for the aws ecs wait command as previously identified.

commcare_connect/solicitations/instructions/implement-plan.md (1)

5-6: Remove or update the "out of date" disclaimer.

This documentation still contains the disclaimer "out of date compared to latest code" that was previously flagged. Either update the documentation to reflect the current implementation or remove it if it no longer serves a purpose, as outdated documentation can confuse maintainers.

commcare_connect/solicitations/migrations/0007_remove_completed_status.py (1)

11-21: Add data migration to convert "completed" status to "closed".

Existing Solicitation records with status="completed" will fail validation after this migration removes the choice. A RunPython operation is needed to bulk-update these records before altering the field definition.

As noted in the previous review, apply this pattern:

 class Migration(migrations.Migration):
     dependencies = [
         ("solicitations", "0006_remove_eoi_rfp_relationships"),
     ]
+    
+    def convert_completed_to_closed(apps, schema_editor):
+        Solicitation = apps.get_model("solicitations", "Solicitation")
+        Solicitation.objects.filter(status="completed").update(status="closed")
 
     operations = [
+        migrations.RunPython(convert_completed_to_closed, migrations.RunPython.noop),
         migrations.AlterField(
             model_name="solicitation",
             name="status",
commcare_connect/opportunity/migrations/0083_alter_uservisit_location.py (1)

11-17: Remove redundant migration.

This migration attempts to alter UserVisit.location to CharField(max_length=255, null=True), but migration 0030 already set this exact definition. Since no intermediate migrations modified this field, this is a no-op that should be removed.

As noted in the previous review, this migration is redundant and should be deleted from the codebase.

commcare_connect/solicitations/admin.py (1)

42-47: Remove nonexistent attachments field from response admin

SolicitationResponse has no attachments field, so loading this admin form raises FieldError: Unknown field(s) (attachments). Please drop the “Files” fieldset (or point it at the actual related name) before merging.

         ("Basic Information", {"fields": ("solicitation", "organization", "submitted_by", "submission_date")}),
         ("Status", {"fields": ("status",)}),
         ("Responses", {"fields": ("responses",), "classes": ("collapse",)}),
-        ("Files", {"fields": ("attachments",)}),
commcare_connect/solicitations/management/commands/load_solicitation_fixtures.py (1)

77-84: Stop referencing the non-existent accepted column on memberships.

UserOrganizationMembership has no accepted field. Passing accepted=True in the defaults (Lines 77‑84) triggers TypeError, and the later queryset filter on memberships__accepted (Lines 162‑164) throws FieldError. Drop those references or gate them until the model actually exposes that column.

Also applies to: 162-164

commcare_connect/solicitations/tests/factories.py (1)

205-210: Factory responses still use the wrong keys.
The form pipeline stores response data keyed by question.question_text, but the factory still emits question_{question.id} keys. Anything relying on realistic factory data (forms, helpers, integration tests) will look up the wrong keys and fail. Please switch to question.question_text for both file and non-file answers.

-                if question.question_type == SolicitationQuestion.Type.FILE:
-                    question_responses[f"question_{question.id}"] = "sample_document.pdf"
-                else:
-                    question_responses[f"question_{question.id}"] = fake.text(max_nb_chars=300)
+                key = question.question_text
+                if question.question_type == SolicitationQuestion.Type.FILE:
+                    question_responses[key] = "sample_document.pdf"
+                else:
+                    question_responses[key] = fake.text(max_nb_chars=300)
commcare_connect/solicitations/management/commands/load_uat_data.py (3)

47-53: Populate DeliveryType slug/description when seeding.
DeliveryType.slug and .description are non-nullable. Calling get_or_create(name=...) leaves them null during the first load, triggering an integrity error. Use the fixture’s slug/description via update_or_create so reruns stay idempotent.

-            dt_name = dt_data["name"]  # Extract name from dict
-            dt, created = DeliveryType.objects.get_or_create(name=dt_name)
+            dt, created = DeliveryType.objects.update_or_create(
+                slug=dt_data["slug"],
+                defaults={
+                    "name": dt_data["name"],
+                    "description": dt_data["description"],
+                },
+            )
             if created:
                 self.stdout.write(f"Created delivery type: {dt.name}")
-            delivery_types[dt.name] = dt
+            delivery_types[dt.name] = dt

80-116: UserOrganizationMembership defaults include unsupported accepted.
UserOrganizationMembership has no accepted field. Passing it through defaults raises TypeError: 'accepted' is an invalid keyword argument on the first run. Remove accepted (here and in the other membership creations on Lines 113-116, 343-357).

-                UserOrganizationMembership.objects.get_or_create(
-                    user=user,
-                    organization=org,
-                    defaults={"role": UserOrganizationMembership.Role.ADMIN, "accepted": True},
-                )
+                UserOrganizationMembership.objects.get_or_create(
+                    user=user,
+                    organization=org,
+                    defaults={"role": UserOrganizationMembership.Role.ADMIN},
+                )

130-139: Program creation omits required fields.
Program.description and Program.currency are required. The current get_or_create call leaves them null, so the initial load fails. Switch to update_or_create with the fixture’s description/currency to stay idempotent.

-            program, created = Program.objects.get_or_create(
+            program, created = Program.objects.update_or_create(
                 slug=prog_data["slug"],
                 defaults={
                     "name": prog_data["name"],
                     "organization": org,
                     "delivery_type": delivery_type,
                     "budget": Decimal(prog_data["budget"]),
+                    "description": prog_data["description"],
+                    "currency": prog_data["currency"],
                     "start_date": date.today(),
                     "end_date": date.today() + timedelta(days=730),
                 },
             )
commcare_connect/solicitations/models.py (2)

111-122: Restore full response status choices.

Migrations and existing rows still rely on the full workflow (draft, submitted, under_review, accepted, rejected, progressed_to_rfp). Restricting the enum to only draft/submitted makes admin/forms reject persisted values and blocks downstream transitions. Please reintroduce the complete set so the model stays aligned with the schema and live data.

     class Status(models.TextChoices):
         DRAFT = "draft", _("Draft")
         SUBMITTED = "submitted", _("Submitted")
+        UNDER_REVIEW = "under_review", _("Under Review")
+        ACCEPTED = "accepted", _("Accepted")
+        REJECTED = "rejected", _("Rejected")
+        PROGRESSED_TO_RFP = "progressed_to_rfp", _("Progressed to RFP")

187-197: Keep review recommendation tokens aligned with the DB.

The migration still stores accept/reject/needs_more_info. Switching the enum to recommended/not_recommended/neutral causes validation failures for existing rows and breaks admin/forms. Please retain the original tokens (or ship a data migration + schema change) so the field remains compatible.

     class Recommendation(models.TextChoices):
-        RECOMMENDED = "recommended", _("Recommended")
-        NOT_RECOMMENDED = "not_recommended", _("Not Recommended")
-        NEUTRAL = "neutral", _("Neutral")
+        ACCEPT = "accept", _("Accept")
+        REJECT = "reject", _("Reject")
+        NEEDS_MORE_INFO = "needs_more_info", _("Needs More Information")
🧹 Nitpick comments (11)
commcare_connect/solicitations/instructions/labs.md (1)

11-30: Consider adding language identifier to fenced code block.

The ASCII diagram at lines 11-30 uses a fenced code block without a language identifier. While not critical for diagrams, adding text or leaving it empty explicitly can satisfy linters.

Apply this diff:

-```
+```text
 ┌─────────────────────┐    ┌─────────────────────┐
 │  connect.dimagi.com │    │ labs.connect.dimagi │
commcare_connect/labs/README.md (1)

209-224: Consider adding language identifier to fenced code block.

The file tree structure at lines 209-224 should specify a language identifier for consistency with linting rules.

Apply this diff:

-```
+```text
 commcare_connect/labs/
 ├── __init__.py
commcare_connect/labs/apps.py (1)

14-30: Consider failing fast when required OAuth settings are missing.

The validation logs errors but allows the application to continue with missing OAuth configuration. This could lead to confusing runtime failures later. Consider raising an ImproperlyConfigured exception when required settings are missing in the labs environment.

Apply this diff:

+from django.core.exceptions import ImproperlyConfigured
+
 def ready(self):
     """Validate labs configuration on startup."""
     # Only validate if we're in labs environment
     if not getattr(settings, "IS_LABS_ENVIRONMENT", False):
         return
 
+    missing_settings = []
+    
     # Check required OAuth settings
     if not getattr(settings, "CONNECT_OAUTH_CLIENT_ID", None):
         logger.error("CONNECT_OAUTH_CLIENT_ID not configured for labs environment")
+        missing_settings.append("CONNECT_OAUTH_CLIENT_ID")
 
     if not getattr(settings, "CONNECT_OAUTH_CLIENT_SECRET", None):
         logger.error("CONNECT_OAUTH_CLIENT_SECRET not configured for labs environment")
+        missing_settings.append("CONNECT_OAUTH_CLIENT_SECRET")
 
     if not getattr(settings, "CONNECT_PRODUCTION_URL", None):
         logger.error("CONNECT_PRODUCTION_URL not configured for labs environment")
+        missing_settings.append("CONNECT_PRODUCTION_URL")
+    
+    if missing_settings:
+        raise ImproperlyConfigured(
+            f"Labs environment requires the following settings: {', '.join(missing_settings)}"
+        )
 
     logger.info("Labs OAuth configuration validated")
commcare_connect/solicitations/instructions/QA-prompt.md (1)

10-10: Optional: Format bare URLs for better Markdown compliance.

The bare URLs on lines 10 and 16 should be wrapped in angle brackets for proper Markdown formatting:

-- **Base URL**: https://flying-kit-curiously.ngrok-free.app/
+- **Base URL**: <https://flying-kit-curiously.ngrok-free.app/>

-- **Spec** (for reference only): https://raw.githubusercontent.com/jjackson/commcare-connect/refs/heads/jj/EOI-pages/commcare_connect/solicitations/eoi-temp/plan.md
+- **Spec** (for reference only): <https://raw.githubusercontent.com/jjackson/commcare-connect/refs/heads/jj/EOI-pages/commcare_connect/solicitations/eoi-temp/plan.md>

Also applies to: 16-16

commcare_connect/audit/management/commands/fix_connect_social_app.py (2)

17-19: Consider making domain configurable for different development environments.

The Site domain is hardcoded to "localhost:8000". Consider making it configurable via a command-line argument for developers using different ports or domains.

  def handle(self, *args, **options):
+     domain = options.get('domain', 'localhost:8000')
      self.stdout.write("\nFixing Connect Social App site association...")
      
      # Get or create the default site
      site, created = Site.objects.get_or_create(
-         pk=settings.SITE_ID, defaults={"domain": "localhost:8000", "name": "Local Development"}
+         pk=settings.SITE_ID, defaults={"domain": domain, "name": "Local Development"}
      )

Add argument parser:

def add_arguments(self, parser):
    parser.add_argument(
        '--domain',
        type=str,
        default='localhost:8000',
        help='Domain for the development site (default: localhost:8000)'
    )

46-48: Consider completing the error handling block.

The exception handler on line 46 provides a helpful error message but doesn't include closing output. Consider adding a closing newline for consistency with the success path (line 44).

  except SocialApp.DoesNotExist:
      self.stdout.write(self.style.ERROR("  ✗ Connect Social App not found!"))
-     self.stdout.write("  You need to create it in Django Admin > Social Applications")
+     self.stdout.write("  You need to create it in Django Admin > Social Applications\n")
commcare_connect/audit/management/extractors/sql_queries.py (2)

7-7: Verify the date in the comment.

The comment date "9/19/2025" appears to be after the PR creation date (2025-09-17). Please verify whether this date is correct or if it should be adjusted to reflect when the query was actually written.


72-72: Document the hardcoded opportunity IDs.

The queries contain hardcoded opportunity IDs (716, 715 on line 72; and a longer list on line 84) without explanation. Consider adding comments to document what these specific opportunities represent (e.g., test data, specific campaigns, etc.) to help future maintainers understand their purpose.

Apply this diff to add documentation:

-# 9/5/2025 Fake Data Analysis
+# 9/5/2025 Fake Data Analysis
+# Opportunity IDs 716, 715 are [add description here]
 SQL_FAKE_DATA_PARTY = """
+# Opportunity IDs for [add description of what these represent]
 SQL_ALL_DATA_QUERY = """

Also applies to: 84-84

commcare_connect/audit/services/assessment_generator.py (1)

50-62: Consider prefixing unused variables with underscore.

The assessment variable returned by get_or_create is unused (lines 50 and 89)—only the created flag is needed for statistics. To silence the linter warning, consider prefixing with an underscore.

Apply this pattern:

-            assessment, created = Assessment.objects.get_or_create(
+            _assessment, created = Assessment.objects.get_or_create(

Also applies to: 89-101

commcare_connect/audit/tables.py (1)

133-174: Avoid mark_safe in table renderers

Both renderers already compose trusted strings, so calling mark_safe just silences our security lint without adding value. Let format_html handle the HTML assembly instead.

@@
-        if value_lower == "pass":
-            badge_class = "badge badge-sm bg-green-50 text-green-700"
-            icon = '<i class="fa-solid fa-thumbs-up mr-1"></i>'
-            text = _("Pass")
-        elif value_lower == "fail":
-            badge_class = "badge badge-sm bg-red-50 text-red-700"
-            icon = '<i class="fa-solid fa-thumbs-down mr-1"></i>'
-            text = _("Fail")
+        if value_lower == "pass":
+            badge_class = "badge badge-sm bg-green-50 text-green-700"
+            icon_class = "fa-solid fa-thumbs-up mr-1"
+            text = _("Pass")
+        elif value_lower == "fail":
+            badge_class = "badge badge-sm bg-red-50 text-red-700"
+            icon_class = "fa-solid fa-thumbs-down mr-1"
+            text = _("Fail")
@@
-        return format_html('<span class="{}">{}{}</span>', badge_class, mark_safe(icon), text)
+        return format_html(
+            '<span class="{}"><i class="{}"></i>{}</span>',
+            badge_class,
+            icon_class,
+            text,
+        )
@@
-        return format_html(
-            '<div class="flex gap-2 justify-end">{}</div>', mark_safe(" ".join(str(btn) for btn in buttons))
-        )
+        buttons_html = format_html(" ".join(["{}"] * len(buttons)), *buttons)
+        return format_html('<div class="flex gap-2 justify-end">{}</div>', buttons_html)
commcare_connect/audit/admin.py (1)

58-71: Cache user_ids before rendering

obj.user_ids executes a fresh values_list(...).distinct() each time, so this block triggers multiple identical queries per row. Cache the lists locally and reuse them to keep the admin detail page snappy.

-        return format_html(
-            "<strong>FLW:</strong> {}<br>"
-            "<strong>Domain:</strong> {}<br>"
-            "<strong>App ID:</strong> {}<br>"
-            "<strong>Opportunity IDs:</strong> {}<br>"
-            "<strong>User IDs:</strong> {}",
-            obj.flw_username or "N/A",
-            obj.domain or "N/A",
-            obj.app_id or "N/A",
-            ", ".join(map(str, obj.opportunity_ids)) if obj.opportunity_ids else "N/A",
-            ", ".join(map(str, obj.user_ids[:5])) + ("..." if len(obj.user_ids) > 5 else "")
-            if obj.user_ids
-            else "N/A",
-        )
+        opportunity_ids = obj.opportunity_ids or []
+        user_ids = obj.user_ids or []
+        user_ids_display = (
+            ", ".join(map(str, user_ids[:5])) + ("..." if len(user_ids) > 5 else "")
+            if user_ids
+            else "N/A"
+        )
+
+        return format_html(
+            "<strong>FLW:</strong> {}<br>"
+            "<strong>Domain:</strong> {}<br>"
+            "<strong>App ID:</strong> {}<br>"
+            "<strong>Opportunity IDs:</strong> {}<br>"
+            "<strong>User IDs:</strong> {}",
+            obj.flw_username or "N/A",
+            obj.domain or "N/A",
+            obj.app_id or "N/A",
+            ", ".join(map(str, opportunity_ids)) if opportunity_ids else "N/A",
+            user_ids_display,
+        )

Comment on lines +108 to +128
- name: Deploy web service
run: |
aws ecs update-service \
--cluster ${{ env.ECS_CLUSTER }} \
--service ${{ env.ECS_WEB_SERVICE }} \
--force-new-deployment \
--output text
- name: Deploy worker service
run: |
aws ecs update-service \
--cluster ${{ env.ECS_CLUSTER }} \
--service ${{ env.ECS_WORKER_SERVICE }} \
--force-new-deployment \
--output text
- name: Wait for services to stabilize
run: |
aws ecs wait services-stable \
--cluster ${{ env.ECS_CLUSTER }} \
--services ${{ env.ECS_WEB_SERVICE }} ${{ env.ECS_WORKER_SERVICE }}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for service deployment steps.

The web and worker service deployment steps lack error handling. If either aws ecs update-service command fails, the workflow continues to the wait step, which may timeout or hang.

Apply this diff:

       - name: Deploy web service
         run: |
           aws ecs update-service \
             --cluster ${{ env.ECS_CLUSTER }} \
             --service ${{ env.ECS_WEB_SERVICE }} \
             --force-new-deployment \
-            --output text
+            --output text || {
+              echo "❌ Failed to update web service"
+              exit 1
+            }
 
       - name: Deploy worker service
         run: |
           aws ecs update-service \
             --cluster ${{ env.ECS_CLUSTER }} \
             --service ${{ env.ECS_WORKER_SERVICE }} \
             --force-new-deployment \
-            --output text
+            --output text || {
+              echo "❌ Failed to update worker service"
+              exit 1
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Deploy web service
run: |
aws ecs update-service \
--cluster ${{ env.ECS_CLUSTER }} \
--service ${{ env.ECS_WEB_SERVICE }} \
--force-new-deployment \
--output text
- name: Deploy worker service
run: |
aws ecs update-service \
--cluster ${{ env.ECS_CLUSTER }} \
--service ${{ env.ECS_WORKER_SERVICE }} \
--force-new-deployment \
--output text
- name: Wait for services to stabilize
run: |
aws ecs wait services-stable \
--cluster ${{ env.ECS_CLUSTER }} \
--services ${{ env.ECS_WEB_SERVICE }} ${{ env.ECS_WORKER_SERVICE }}
- name: Deploy web service
run: |
aws ecs update-service \
--cluster ${{ env.ECS_CLUSTER }} \
--service ${{ env.ECS_WEB_SERVICE }} \
--force-new-deployment \
--output text || {
echo "❌ Failed to update web service"
exit 1
}
- name: Deploy worker service
run: |
aws ecs update-service \
--cluster ${{ env.ECS_CLUSTER }} \
--service ${{ env.ECS_WORKER_SERVICE }} \
--force-new-deployment \
--output text || {
echo "❌ Failed to update worker service"
exit 1
}
- name: Wait for services to stabilize
run: |
aws ecs wait services-stable \
--cluster ${{ env.ECS_CLUSTER }} \
--services ${{ env.ECS_WEB_SERVICE }} ${{ env.ECS_WORKER_SERVICE }}
🤖 Prompt for AI Agents
In .github/workflows/deploy-labs.yml around lines 108 to 128, the two ecs
update-service steps run without checking failure and the workflow will continue
to the wait step even if deployments fail; modify each deploy step's run block
to enable strict bash error handling (e.g., start with set -euo pipefail) or
explicitly test the aws command exit status and exit with a non-zero code and a
descriptive error message on failure (for example: if ! aws ecs update-service
...; then echo "Error updating <web|worker> service" >&2; exit 1; fi), so the
workflow stops immediately when an update-service command fails and the wait
step only runs on success.

Comment on lines +124 to +128
- name: Wait for services to stabilize
run: |
aws ecs wait services-stable \
--cluster ${{ env.ECS_CLUSTER }} \
--services ${{ env.ECS_WEB_SERVICE }} ${{ env.ECS_WORKER_SERVICE }}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add timeout to service stabilization wait.

The aws ecs wait services-stable command has no explicit timeout and could hang indefinitely if services fail to stabilize. Add a timeout to prevent workflow hangs.

Apply this diff:

       - name: Wait for services to stabilize
         run: |
+          echo "⏳ Waiting for services to stabilize (timeout: 10 minutes)..."
-          aws ecs wait services-stable \
+          timeout 600 aws ecs wait services-stable \
             --cluster ${{ env.ECS_CLUSTER }} \
-            --services ${{ env.ECS_WEB_SERVICE }} ${{ env.ECS_WORKER_SERVICE }}
+            --services ${{ env.ECS_WEB_SERVICE }} ${{ env.ECS_WORKER_SERVICE }} || {
+              echo "❌ Services failed to stabilize within timeout"
+              exit 1
+            }
🤖 Prompt for AI Agents
.github/workflows/deploy-labs.yml around lines 124-128: the aws ecs wait
services-stable command can hang indefinitely; wrap the wait command with a
timeout (e.g. prefix with timeout 10m) so the step fails if services don't
stabilize within the window, and ensure the step treats a non-zero exit as
failure (leave the pipeline to fail or add an explicit check); replace the run
block to use timeout 10m aws ecs wait services-stable --cluster ... --services
... and optionally handle the timeout exit code to surface a clear error
message.

Comment on lines +58 to +73
return (
UserVisit.objects.filter(
user=flw_user,
opportunity=opportunity,
visit_date__date__range=[start_date, end_date],
status=VisitValidationStatus.approved,
)
.exclude(
# Exclude visits that don't have images
Q(images__isnull=True)
| Q(images__content_type__isnull=True)
)
.distinct()
.prefetch_related("images")
.order_by("visit_date")
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove ORM lookups on the images property.

UserVisit.images is a computed property (see opportunity models), not a related field. Filtering with images__… and calling prefetch_related("images") therefore throws FieldError: Cannot resolve keyword 'images' into field before any audit session can be validated. Rework this lookup—e.g., join to the concrete blob meta model or post-filter in Python—so the queryset executes without referencing the property.

Comment on lines +11 to +12
from commcare_connect.audit.oauth_views import ConnectOAuth2Adapter

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix adapter import to avoid crash

ConnectOAuth2Adapter lives in commcare_connect.audit.provider, not oauth_views. This import raises ImportError, so the management command can’t even start. Update the import:

-from commcare_connect.audit.oauth_views import ConnectOAuth2Adapter
+from commcare_connect.audit.provider import ConnectOAuth2Adapter
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from commcare_connect.audit.oauth_views import ConnectOAuth2Adapter
from commcare_connect.audit.provider import ConnectOAuth2Adapter
🤖 Prompt for AI Agents
In commcare_connect/audit/management/commands/debug_oauth_flow.py around lines
11 to 12, the command imports ConnectOAuth2Adapter from
commcare_connect.audit.oauth_views which raises ImportError; update the import
to pull ConnectOAuth2Adapter from commcare_connect.audit.provider instead so the
management command can load without crashing.

Comment on lines +937 to +959
def _get_visit_count_preview_superset(self, params: AuditParameters) -> dict[str, int]:
"""Get visit count preview using Superset data."""
flw_ids_str = ",".join(map(str, params.flw_ids))

sql = f"""
SELECT
status,
COUNT(*) as count
FROM opportunity_uservisit
WHERE opportunity_id = {params.opportunity_id}
AND user_id IN ({flw_ids_str})
AND visit_date::date >= '{params.start_date}'
AND visit_date::date <= '{params.end_date}'
"""

if not params.include_test_data:
sql += " AND opportunity_id NOT IN (SELECT id FROM opportunity_opportunity WHERE is_test = true)"

if params.include_flagged_only:
sql += " AND flagged = true"

sql += " GROUP BY status"

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle empty FLW lists before building SQL
When params.flw_ids is empty we produce user_id IN (), which Superset rejects. Guard for the empty case (return {"total": 0} early or skip the IN clause) before formatting the SQL. Apply the same fix to _download_audit_data_superset, which builds the same clause.

🧰 Tools
🪛 Ruff (0.14.3)

941-950: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
In commcare_connect/audit/management/extractors/connect_api_facade.py around
lines 937 to 959, the code builds a SQL fragment "user_id IN (...)" even when
params.flw_ids is empty which produces "user_id IN ()" and breaks Superset; add
a guard that checks if not params.flw_ids and return {"total": 0} immediately
(or alternatively skip adding the IN clause), then proceed to build the SQL only
when flw_ids is non-empty; apply the same empty-list guard/fix to the
_download_audit_data_superset function that constructs the same IN clause.

Comment on lines +156 to +168
solicitation, created = Solicitation.objects.get_or_create(
title=sol_data["title"],
defaults={
"program": program,
"description": sol_data["description"],
"target_population": sol_data.get("target_population", "General population"),
"estimated_scale": sol_data.get("estimated_scale", "Regional"),
"status": sol_data.get("status", "active"),
"solicitation_type": sol_data.get("solicitation_type", "eoi"),
"application_deadline": date.today() + timedelta(days=30),
},
)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Scope of work is missing from solicitation defaults.
Solicitation.scope_of_work is required (null=False, blank=False). Attempting to create a solicitation without it raises an IntegrityError. Please load the YAML value (or provide a safe fallback) and include it in the defaults.

                     "description": sol_data["description"],
                     "target_population": sol_data.get("target_population", "General population"),
+                    "scope_of_work": sol_data["scope_of_work"],
                     "estimated_scale": sol_data.get("estimated_scale", "Regional"),
🤖 Prompt for AI Agents
In commcare_connect/solicitations/management/commands/load_uat_data.py around
lines 156 to 168, the Solicitation.get_or_create defaults are missing the
required field scope_of_work which causes an IntegrityError; read scope_of_work
from sol_data (e.g. sol_data.get("scope_of_work")) and include it in the
defaults dictionary, falling back to a safe non-empty string (for example "Scope
not provided" or the YAML description) so the created Solicitation meets
null=False/blank=False constraints.

Comment on lines +44 to +140
current_allowed_hosts = list(settings.ALLOWED_HOSTS)
if "testserver" not in current_allowed_hosts:
settings.ALLOWED_HOSTS = current_allowed_hosts + ["testserver"]

# Create test client and login
client = Client()
client.force_login(user)

self.stdout.write(f"\n=== Testing Unified Dashboard for {user.email} ===")

def test_url(url, description):
try:
response = client.get(url, follow=True)
if response.status_code == 200:
self.stdout.write(f"✓ {description}")
return response
else:
self.stdout.write(self.style.ERROR(f"✗ {description}: HTTP {response.status_code}"))
return None
except Exception as e:
self.stdout.write(self.style.ERROR(f"✗ {description}: {str(e)}"))
return None

def extract_solicitation_links(html_content):
"""Extract solicitation-related action links from HTML"""
links = []
href_pattern = r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>'

for match in re.finditer(href_pattern, html_content, re.DOTALL | re.IGNORECASE):
href = match.group(1)
link_content = match.group(2)

# Only include solicitation-related links, but exclude sort links
if ("/solicitations/" in href or href.startswith("/a/")) and "sort=" not in href:
# Get description from icon or text
description = "Action"
if "fa-eye" in link_content:
description = "View"
elif "fa-edit" in link_content or "fa-pen-to-square" in link_content:
description = "Edit"
elif "fa-inbox" in link_content:
description = "View Responses"
elif "Create" in link_content:
description = "Create"

links.append((href, description))

return links

def extract_pagination_links(html_content):
"""Extract pagination links from django-tables2 pagination"""
pagination_links = []

# Look for pagination links in django-tables2 format
pagination_pattern = r'<a[^>]+href=["\']([^"\']*\?[^"\']*page=[^"\']*)["\'][^>]*>'

for match in re.finditer(pagination_pattern, html_content):
href = match.group(1)
parsed_url = urlparse(href)
query_params = parse_qs(parsed_url.query)

if "page" in query_params:
page_num = query_params["page"][0]
pagination_links.append((href, f"Page {page_num}"))

return pagination_links

# 1. Test Main Dashboard
self.stdout.write("\n--- Testing Main Dashboard ---")
dashboard_response = test_url("/solicitations/dashboard/", "Main dashboard")
if not dashboard_response:
return

dashboard_content = dashboard_response.content.decode()

if not pagination_only:
# 2. Test all action links on dashboard
self.stdout.write("\n--- Testing Dashboard Action Links ---")
dashboard_links = extract_solicitation_links(dashboard_content)

for url, description in dashboard_links:
if not test_url(url, f"Dashboard {description}: {url}"):
return

# 3. Test Pagination (if pagination links exist on the page)
self.stdout.write("\n--- Testing Pagination ---")

# Extract pagination links from the main dashboard page
pagination_links = extract_pagination_links(dashboard_content)

if pagination_links:
# Test the first few pagination links found on the page
for url, desc in pagination_links[:5]: # Test first 5 pagination links
if not test_url(url, f"Pagination: {desc}"):
return
else:
self.stdout.write("No pagination links found on dashboard")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore ALLOWED_HOSTS even when the command exits early.

The command mutates settings.ALLOWED_HOSTS, but every failure path after this point returns immediately, so the original value is never restored. When this command is invoked via call_command inside the test suite (or any long-lived process), the mutated host list leaks into subsequent code, potentially hiding genuine host configuration regressions. Please wrap the rest of the logic in a try/finally and restore the original list in the finally block so we leave global settings exactly as we found them.

-        # Temporarily add testserver to ALLOWED_HOSTS for testing
-        current_allowed_hosts = list(settings.ALLOWED_HOSTS)
-        if "testserver" not in current_allowed_hosts:
-            settings.ALLOWED_HOSTS = current_allowed_hosts + ["testserver"]
-
-        # Create test client and login
-        client = Client()
-        client.force_login(user)
-
-        self.stdout.write(f"\n=== Testing Unified Dashboard for {user.email} ===")
+        current_allowed_hosts = list(settings.ALLOWED_HOSTS)
+        if "testserver" not in current_allowed_hosts:
+            settings.ALLOWED_HOSTS = [*current_allowed_hosts, "testserver"]
+
+        try:
+            client = Client()
+            client.force_login(user)
+
+            self.stdout.write(f"\n=== Testing Unified Dashboard for {user.email} ===")
@@
-        # Restore original ALLOWED_HOSTS
-        settings.ALLOWED_HOSTS = current_allowed_hosts
-
-        self.stdout.write(self.style.SUCCESS("All tests passed! Dashboard working correctly."))
+        finally:
+            settings.ALLOWED_HOSTS = current_allowed_hosts

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.14.3)

46-46: Consider [*current_allowed_hosts, "testserver"] instead of concatenation

Replace with [*current_allowed_hosts, "testserver"]

(RUF005)


63-63: Do not catch blind exception: Exception

(BLE001)


64-64: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In commcare_connect/solicitations/management/commands/test_dashboard_links.py
around lines 44 to 140, the command mutates settings.ALLOWED_HOSTS but returns
early on failures without restoring the original value; capture the original
settings.ALLOWED_HOSTS into a variable before changing it, wrap the remaining
logic (client creation, tests, extraction and all early returns) in a try block,
and put a finally block that restores settings.ALLOWED_HOSTS to the saved
original (use a shallow copy/list to avoid aliasing) so the global settings are
always reset even if the command returns or raises.

Comment on lines +11 to +35
old_statuses = ['under_review', 'accepted', 'rejected']
for old_status in old_statuses:
SolicitationResponse.objects.filter(status=old_status).update(status='submitted')


def reverse_convert_old_statuses(apps, schema_editor):
"""Reverse migration - not possible to restore original statuses"""
# We can't restore the original statuses since we don't know what they were
pass


class Migration(migrations.Migration):
dependencies = [
("solicitations", "0007_remove_completed_status"),
]

operations = [
# First convert the data
migrations.RunPython(convert_old_statuses, reverse_convert_old_statuses, hints={'run_on_secondary': False}),
# Then alter the field
migrations.AlterField(
model_name="solicitationresponse",
name="status",
field=models.CharField(
choices=[("draft", "Draft"), ("submitted", "Submitted")], default="draft", max_length=20
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Handle legacy progressed_to_rfp statuses before tightening choices
convert_old_statuses() only rewrites under_review, accepted, and rejected. We still have rows with the progressed_to_rfp value introduced earlier (see 0002). After this data migration, those rows remain, and the subsequent AlterField to choices [("draft", "Draft"), ("submitted", "Submitted")] will fail and roll back the deploy. Please fold progressed_to_rfp into the conversion (and any other legacy status we expect) before altering the field.

-    old_statuses = ['under_review', 'accepted', 'rejected']
+    old_statuses = ['under_review', 'accepted', 'rejected', 'progressed_to_rfp']
🧰 Tools
🪛 Ruff (0.14.3)

23-25: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🤖 Prompt for AI Agents
In commcare_connect/solicitations/migrations/0008_simplify_response_status.py
around lines 11 to 35, convert_old_statuses currently only maps
['under_review','accepted','rejected'] to 'submitted' but misses legacy
'progressed_to_rfp' (and any other old statuses), causing the subsequent
AlterField choices tighten to fail; update the old_statuses list to include
'progressed_to_rfp' (and any other expected legacy values) so those rows are
also updated to 'submitted' before the AlterField runs, keeping
reverse_convert_old_statuses as a no-op.

Comment on lines +41 to +48
def render_two_line_text(title, subtitle=None):
"""Render two-line text content"""
if subtitle:
return format_html(
'<div><div class="font-medium">{}</div><div class="text-xs text-gray-500">{}</div></div>',
title,
mark_safe(subtitle) if isinstance(subtitle, str) and ("<" in subtitle and ">" in subtitle) else subtitle,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove unsafe mark_safe usage on subtitles.
render_two_line_text calls mark_safe on any string containing </>. Program descriptions are rich-text fields sourced from user input, so this bypasses Django’s escaping and lets a stored <script> (or any HTML payload) execute in dashboards. Let format_html escape the subtitle unless you’ve already sanitized it upstream.

-            mark_safe(subtitle) if isinstance(subtitle, str) and ("<" in subtitle and ">" in subtitle) else subtitle,
+            subtitle,
🧰 Tools
🪛 Ruff (0.14.3)

47-47: Use of mark_safe may expose cross-site scripting vulnerabilities

(S308)

🤖 Prompt for AI Agents
In commcare_connect/solicitations/tables.py around lines 41–48, remove the
unsafe mark_safe call on raw subtitle strings; instead only treat subtitles as
already-safe if they are an explicit SafeString (from django.utils.safestring
import SafeString), otherwise pass the subtitle through to format_html (or
str(subtitle)) so format_html will escape it. Concretely: drop the
mark_safe(...) branch that checks for "<" and ">", replace it with an
isinstance(subtitle, SafeString) check to allow previously-sanitized HTML, and
ensure non-safe subtitles are passed unchanged so format_html performs proper
escaping.

Comment on lines +1 to +210
# 🚀 CommCare Connect - Quick Start Guide

This guide will help you run the CommCare Connect app every time you want to see it.

## 📋 Prerequisites

- **Python 3.11** installed ✅
- **Docker & Docker Compose** installed (optional - app can run with SQLite)
- **Node.js & npm** installed ✅
- **Git** installed ✅

## 🏃‍♂️ Quick Start (Every Time You Want to Run the App)

### 1. Navigate to Project Directory
```bash
cd /Users/sarveshtewari/commcare-connect
```

### 2. Activate Virtual Environment
```bash
source .venv/bin/activate
```

### 3. Start Services (Optional - for PostgreSQL/Redis)
```bash
# If you have Docker installed, start database services:
inv up

# If no Docker, the app will use SQLite (already configured)
```

### 4. Start the Django Server
```bash
python manage.py runserver
```

### 5. Open Your Browser
Navigate to: **http://localhost:8000**

---

## 🚀 **Super Quick Start (No Docker Required)**

If you just want to run the app quickly without Docker:

```bash
cd /Users/sarveshtewari/commcare-connect
source .venv/bin/activate
python manage.py runserver
```

Then open: **http://localhost:8000**

---

## 🔧 First-Time Setup (Only Once)

If this is your first time running the app, follow these additional steps:

### 1. Install Python Dependencies
```bash
# Activate virtual environment first
source .venv/bin/activate

# Install requirements
pip install -r requirements-dev.txt
```

### 2. Install JavaScript Dependencies
```bash
npm ci
```

### 3. Build JavaScript Assets
```bash
# Build once
inv build-js

# Or build and watch for changes
inv build-js -w
```

### 4. Set Up Environment File
```bash
# Copy template if you haven't already
cp .env_template .env

# Edit .env file if needed (usually not required for local development)
```

### 5. Run Database Migrations
```bash
python manage.py migrate
```

### 6. Load Sample Data (Optional)
```bash
# Load solicitation data for testing
python manage.py load_uat_data
```

### 7. Create Superuser (Optional)
```bash
python manage.py createsuperuser
```

---

## 🛑 Stopping the App

### Stop Django Server
Press `Ctrl+C` in the terminal where Django is running

### Stop Docker Services
```bash
# Option A: Using invoke
inv down

# Option B: Using docker-compose directly
docker compose down
```

### Deactivate Virtual Environment
```bash
deactivate
```

---

## 🔍 Troubleshooting

### Database Connection Issues
```bash
# Check if Docker services are running
docker compose ps

# Restart Docker services
docker compose down
docker compose up -d
```

### Python Dependencies Issues
```bash
# Reinstall requirements
source .venv/bin/activate
pip install -r requirements-dev.txt
```

### JavaScript Build Issues
```bash
# Reinstall npm packages
rm -rf node_modules package-lock.json
npm ci
inv build-js
```

### Port Already in Use
If port 8000 is already in use:
```bash
# Use a different port
python manage.py runserver 8001
```

---

## 📱 Accessing the App

Once running, you can access:

- **Main App**: http://localhost:8000
- **Admin Panel**: http://localhost:8000/admin
- **Solicitations**: http://localhost:8000/solicitations/
- **EOIs Only**: http://localhost:8000/solicitations/eoi/
- **RFPs Only**: http://localhost:8000/solicitations/rfp/

---

## 🎯 Common Commands Reference

| Command | Purpose |
|---------|---------|
| `inv up` | Start Docker services |
| `inv down` | Stop Docker services |
| `inv build-js` | Build JavaScript assets |
| `inv build-js -w` | Build and watch JS for changes |
| `python manage.py runserver` | Start Django development server |
| `python manage.py migrate` | Run database migrations |
| `python manage.py load_uat_data` | Load sample solicitation data |
| `python manage.py createsuperuser` | Create admin user |

---

## 💡 Pro Tips

1. **Keep Docker Running**: You can leave Docker services running between development sessions
2. **Use Two Terminals**: Keep one terminal for Django server, another for other commands
3. **Watch Mode**: Use `inv build-js -w` to automatically rebuild JS when files change
4. **Sample Data**: The `load_uat_data` command creates realistic test data for solicitations

---

## 🆘 Need Help?

- Check the main [README.md](README.md) for detailed setup instructions
- Look at the [tasks.py](tasks.py) file for available commands
- Ensure all prerequisites are installed correctly

---

**Happy coding! 🎉**
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix Prettier formatting to resolve CI pipeline failure.

The Prettier formatting check failed for this file. Run prettier --write QUICK_START_GUIDE.md to auto-format the file according to the project's style guidelines.

#!/bin/bash
# Fix prettier formatting
npx prettier --write QUICK_START_GUIDE.md
🧰 Tools
🪛 GitHub Actions: CI

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

🪛 LanguageTool

[style] ~12-~12: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...✅ ## 🏃‍♂️ Quick Start (Every Time You Want to Run the App) ### 1. Navigate to Projec...

(REP_WANT_TO_VB)

🪛 markdownlint-cli2 (0.18.1)

38-38: Bare URL used

(MD034, no-bare-urls)


52-52: Bare URL used

(MD034, no-bare-urls)


170-170: Bare URL used

(MD034, no-bare-urls)


171-171: Bare URL used

(MD034, no-bare-urls)


172-172: Bare URL used

(MD034, no-bare-urls)


173-173: Bare URL used

(MD034, no-bare-urls)


174-174: Bare URL used

(MD034, no-bare-urls)


210-210: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🤖 Prompt for AI Agents
In QUICK_START_GUIDE.md (lines 1-210) the file fails the Prettier formatting
check; run the project's Prettier formatter (e.g., npx prettier --write
QUICK_START_GUIDE.md or prettier --write QUICK_START_GUIDE.md), review the
formatted changes, stage and commit the updated file, and push the commit to
update the PR so CI formatting check passes.

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.

2 participants