import types
|
|
import projects.models as project_models
|
import pytest
|
from django.db import connection
|
from projects.models import Project
|
|
from label_studio.core.utils import db as db_utils
|
from label_studio.core.utils.db import has_column_cached
|
from label_studio.organizations.tests.factories import OrganizationFactory
|
from label_studio.projects.tests.factories import ProjectFactory
|
|
pytestmark = pytest.mark.django_db
|
|
|
def test_project_manager_filters_deleted():
|
"""Project manager hides deleted rows by default; unfiltered manager returns all.
|
|
Purpose: Verify default manager excludes soft-deleted rows and all_objects returns them.
|
Setup: Create two projects in one org; mark one as deleted.
|
Actions: Query via Project.objects and Project.all_objects.
|
Validations: Visible list excludes deleted; unfiltered includes both.
|
Edge cases: N/A.
|
"""
|
org = OrganizationFactory()
|
p1 = ProjectFactory(organization=org, title='active')
|
_ = ProjectFactory(organization=org, title='deleted', deleted_at=p1.created_at)
|
|
visible = list(Project.objects.order_by('id').values_list('title', flat=True))
|
all_rows = list(Project.all_objects.order_by('id').values_list('title', flat=True))
|
|
assert 'active' in visible and 'deleted' not in visible
|
assert set(all_rows) >= {'active', 'deleted'}
|
|
|
def test_project_manager_for_user_respects_filter():
|
"""for_user applies org scope and soft-delete filter.
|
|
Purpose: Ensure for_user(user) scopes to user's active org and hides deleted rows.
|
Setup: Two orgs; three projects (active+deleted in org1, active in org2).
|
Actions: Call Project.objects.for_user(user) for org1 user.
|
Validations: Only org1 active project is returned.
|
Edge cases: N/A.
|
"""
|
org1 = OrganizationFactory()
|
org2 = OrganizationFactory()
|
user = org1.created_by
|
user.active_organization = org1
|
user.save(update_fields=['active_organization'])
|
|
p1 = ProjectFactory(organization=org1, title='org1-active')
|
_ = ProjectFactory(organization=org1, title='org1-deleted', deleted_at=p1.created_at)
|
_ = ProjectFactory(organization=org2, title='org2-active')
|
|
titles = set(Project.objects.for_user(user).values_list('title', flat=True))
|
assert 'org1-active' in titles
|
assert 'org1-deleted' not in titles
|
assert 'org2-active' not in titles
|
|
|
def test_visible_manager_skips_filter_without_column(monkeypatch):
|
"""Manager should not reference missing deleted_at during early migrations.
|
|
Purpose: Avoid schema errors before column exists.
|
Setup: Force has_column_cached to return False; create active+deleted rows.
|
Actions: Query via Project.objects.
|
Validations: Both rows are returned (no filter applied).
|
Edge cases: N/A.
|
"""
|
monkeypatch.setattr(project_models, 'has_column_cached', lambda *_: False, raising=True)
|
|
org = OrganizationFactory()
|
p1 = ProjectFactory(organization=org, title='active')
|
_ = ProjectFactory(organization=org, title='deleted', deleted_at=p1.created_at)
|
|
# Without column, the filter is skipped, so both come back
|
titles = set(Project.objects.values_list('title', flat=True))
|
assert {'active', 'deleted'} <= titles
|
|
|
def test_has_column_cached_memoization_and_clear(monkeypatch):
|
"""Column presence check is memoized and reset by post_migrate.
|
|
Purpose: Ensure only one introspection call until cache clear.
|
Setup: Stub get_table_description to count calls.
|
Actions: Call has_column_cached twice, then clear cache, call again.
|
Validations: One call before, second after clear.
|
Edge cases: N/A.
|
"""
|
# Ensure cache starts empty so first call triggers introspection
|
db_utils.signal_clear_column_presence_cache()
|
|
calls = {'count': 0}
|
|
def fake_get_table_description(cursor, table):
|
calls['count'] += 1
|
# Return objects that mimic description entries with .name attribute
|
col = types.SimpleNamespace(name='deleted_at')
|
return [col]
|
|
monkeypatch.setattr(connection.introspection, 'get_table_description', fake_get_table_description)
|
|
# First call hits DB introspection
|
assert has_column_cached('project', 'deleted_at') is True
|
# Second call should be cached
|
assert has_column_cached('project', 'deleted_at') is True
|
assert calls['count'] == 1
|
|
# Clear cache directly (instead of sending the signal with app_config)
|
db_utils.signal_clear_column_presence_cache()
|
|
# After cache clear, another introspection happens
|
assert has_column_cached('project', 'deleted_at') is True
|
assert calls['count'] == 2
|