Skip to content

Commit f8d63d4

Browse files
ewdurbinclaude
andauthored
orgs: add ability to mark org as active manually (#18604)
* feat(admin): add manual organization activation with seat limits Add comprehensive manual activation system for organizations with seat limits and expiration dates: - Add OrganizationManualActivation model with seat limits, expiration tracking, and current member count - Extend organization good_standing property to check manual activation status - Create admin interface for managing manual activations (add/update/delete) - Implement seat limit enforcement in invitation sending (manage/views/organizations.py) - Implement seat limit enforcement in invitation acceptance (accounts/views.py) - Add comprehensive test coverage for seat limit enforcement scenarios - Include database migration for new manual activation table - Add factory support for testing manual activations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix(organizations): improve manual activation UI and change expiration to date - Change expiration field from datetime to date for simpler management - Fix UI issues where manually activated orgs still showed billing prompts - Add billing transparency and seat limit enforcement to member invites - Fix template crashes when accessing settings for manual activations - Improve seat usage messaging and remove duplicate warning boxes - Update predicates to properly handle manual activation access - Make tests durable by using freezegun instead of hardcoded dates 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor(organizations): consolidate status checking logic with better naming Consolidate all duplicated organization status checking logic into clear, well-named methods to eliminate code duplication and improve maintainability. **Key Changes:** - Add consolidated status methods: is_in_good_standing(), can_invite_new_members(), has_active_billing(), get_billing_status_display() - Refactor good_standing property to use consolidated logic for consistency - Update predicates to use consolidated is_in_good_standing() method - Consolidate seat limit enforcement in both invitation flows - Standardize billing status display across forms - Improve method naming: operational → in_good_standing, accept → invite **Enhanced Security:** - Company organizations now consistently require active billing to perform actions - Fixed test that incorrectly allowed Company orgs without billing to invite members **Testing:** - Add comprehensive test coverage for all consolidated methods (100% coverage) - Add tests for organizations not in good standing scenarios - All 4931 tests passing with complete coverage **Files Modified:** - warehouse/organizations/models.py: Add consolidated status methods - warehouse/predicates.py: Use consolidated logic, remove unused imports - warehouse/accounts/views.py: Use consolidated seat limit enforcement - warehouse/manage/views/organizations.py: Use consolidated methods - warehouse/manage/forms.py: Use consolidated billing status display - tests/: Add comprehensive coverage for all new functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: eliminate over-mocking in seat limit tests - Replace extensive service mocking with real database services - Use DatabaseOrganizationService for actual DB operations - Use DatabaseUserService for real user lookups (thanks for the catch!) - Create shared fixtures for common test scenarios - Reduce test complexity from 350+ lines to ~240 lines - Tests now verify actual database state, not mocked behavior - Maintain 100% test coverage requirement - All 4931 tests still passing This follows the guidance to use 'simple minimal tests that utilize actual database objects and factories and the actual services rather than leaning too far into stopping and mocking.' 🤖 Generated with Claude Code Co-Authored-By: Claude <[email protected]> * refactor: consolidate organization status checks and eliminate code duplication - Remove redundant has_active_billing() method (duplicated is_in_good_standing() logic) - Update get_billing_status_display() to use canonical is_in_good_standing() method - Replace complex template conditionals with consolidated business logic methods - Simplify templates by using organization.is_in_good_standing() and organization.can_invite_new_members() - Update test methods to use the canonical is_in_good_standing() instead of removed has_active_billing() This eliminates ~50 lines of duplicated logic and ensures single source of truth for organization status checking throughout the application. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * refactor: allow invitations to exceed seat limits with transparency Changed seat limits from blocking to informational only: - Organization invitations can now be accepted even when over seat limit - Removed redundant can_invite_new_members() method - Seat limits are shown as informational messages in UI - Only 'good standing' (billing/expiration) blocks invitations - Simplified test structure and removed overcomplicated fixtures - Updated translation strings and achieved 100% test coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * feat: allow invitation acceptance for organizations not in good standing - Remove good standing check from invitation acceptance in accounts views - Users can now accept invitations even if organization has billing issues - Keep good standing check for sending invitations (prevents expansion) - Remove test for the removed functionality - Fix linting issues This allows invited members to join organizations with temporary billing issues while still preventing problematic organizations from expanding by sending new invitations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * style: align OrganizationManualActivation model with codebase conventions - Remove unnecessary Integer and Date imports (SQLAlchemy infers from types) - Remove forward reference quotes for relationships (pyupgrade handles this) - Use simpler mapped_column syntax without explicit type imports - Follow same patterns as other models in the codebase 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * fix: regenerate migration after model typing updates - Rollback database to before manual activation migration - Delete old migration file - Regenerate migration with updated model (no Integer/Date imports) - New migration uses cleaner column definitions matching conventions - All tests pass with 100% coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * chore: update translation files - Update messages.pot with removed string - Remove "Cannot accept invitation. Organization is not in good standing." translation string since we no longer block invitation acceptance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 4c28b41 commit f8d63d4

File tree

18 files changed

+1641
-122
lines changed

18 files changed

+1641
-122
lines changed

tests/common/db/organizations.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
OrganizationApplication,
1212
OrganizationApplicationStatus,
1313
OrganizationInvitation,
14+
OrganizationManualActivation,
1415
OrganizationNameCatalog,
1516
OrganizationProject,
1617
OrganizationRole,
@@ -131,6 +132,20 @@ class Meta:
131132
organization = factory.SubFactory(OrganizationFactory)
132133

133134

135+
class OrganizationManualActivationFactory(WarehouseFactory):
136+
class Meta:
137+
model = OrganizationManualActivation
138+
139+
organization_id = factory.SelfAttribute("organization.id")
140+
organization = factory.SubFactory(OrganizationFactory)
141+
seat_limit = 10
142+
expires = factory.LazyFunction(
143+
lambda: datetime.date.today() + datetime.timedelta(days=365)
144+
)
145+
created_by_id = factory.SelfAttribute("created_by.id")
146+
created_by = factory.SubFactory(UserFactory)
147+
148+
134149
class OrganizationProjectFactory(WarehouseFactory):
135150
class Meta:
136151
model = OrganizationProject

tests/unit/admin/test_routes.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ def test_includeme():
4444
"/admin/organizations/{organization_id}/delete_role/{role_id}/",
4545
domain=warehouse,
4646
),
47+
pretend.call(
48+
"admin.organization.add_manual_activation",
49+
"/admin/organizations/{organization_id}/add_manual_activation/",
50+
domain=warehouse,
51+
),
52+
pretend.call(
53+
"admin.organization.update_manual_activation",
54+
"/admin/organizations/{organization_id}/update_manual_activation/",
55+
domain=warehouse,
56+
),
57+
pretend.call(
58+
"admin.organization.delete_manual_activation",
59+
"/admin/organizations/{organization_id}/delete_manual_activation/",
60+
domain=warehouse,
61+
),
4762
pretend.call(
4863
"admin.organization_application.list",
4964
"/admin/organization_applications/",

0 commit comments

Comments
 (0)