Spaces:
Sleeping
Sleeping
| import pytest | |
| from django.contrib.auth import get_user_model | |
| from django.contrib.admin.sites import AdminSite | |
| from django.contrib.messages.storage.fallback import FallbackStorage | |
| from django.test import RequestFactory | |
| from rest_framework import status | |
| from rest_framework.test import APIClient | |
| from apps.resources.admin import ResourceAdmin, ResourceAdminForm | |
| from apps.resources.models import Resource, ResourceCheckpoint, SkillResource | |
| from apps.skills.models import Skill | |
| def _admin_request(method='post', path='/admin/'): | |
| """Build a RequestFactory request with messages storage wired up so | |
| admin views that call self.message_user don't explode.""" | |
| request = getattr(RequestFactory(), method)(path) | |
| setattr(request, 'session', {}) | |
| setattr(request, '_messages', FallbackStorage(request)) | |
| return request | |
| User = get_user_model() | |
| pytestmark = pytest.mark.django_db | |
| def user(): | |
| return User.objects.create_user( | |
| username='r@x.com', email='r@x.com', password='pw', name='R', | |
| ) | |
| def auth_client(user): | |
| c = APIClient() | |
| c.force_authenticate(user=user) | |
| return c | |
| def python_skill(): | |
| return Skill.objects.create(skill_name='Python', category='Programming', difficulty_level='BEGINNER') | |
| def sql_skill(): | |
| return Skill.objects.create(skill_name='SQL', category='Database', difficulty_level='BEGINNER') | |
| def resource(python_skill): | |
| r = Resource.objects.create( | |
| title='Python for Everybody', | |
| provider='Coursera', | |
| url='https://coursera.org/python-for-everybody', | |
| difficulty_level='BEGINNER', | |
| duration=1200, | |
| type='COURSE', | |
| rating=4.7, | |
| ) | |
| SkillResource.objects.create(skill=python_skill, resource=r, relevance_score=0.95) | |
| return r | |
| class TestResourceAPI: | |
| def test_list_requires_auth(self): | |
| assert APIClient().get('/api/resources/').status_code == status.HTTP_401_UNAUTHORIZED | |
| def test_list_resources(self, auth_client, resource): | |
| response = auth_client.get('/api/resources/') | |
| assert response.status_code == status.HTTP_200_OK | |
| assert response.data['count'] == 1 | |
| results = response.data['results'] | |
| assert results[0]['title'] == 'Python for Everybody' | |
| assert results[0]['has_checkpoints'] is False | |
| def test_filter_by_skill(self, auth_client, resource, python_skill, sql_skill): | |
| # Another resource mapped to SQL only. | |
| other = Resource.objects.create( | |
| title='SQL Tutorial', provider='W3Schools', | |
| url='https://w3schools.com/sql', | |
| difficulty_level='BEGINNER', duration=60, type='DOCS', | |
| ) | |
| SkillResource.objects.create(skill=sql_skill, resource=other, relevance_score=0.8) | |
| response = auth_client.get(f'/api/resources/?skill={python_skill.id}') | |
| assert response.status_code == status.HTTP_200_OK | |
| assert response.data['count'] == 1 | |
| assert response.data['results'][0]['id'] == resource.id | |
| def test_filter_by_type_and_level(self, auth_client, resource): | |
| Resource.objects.create( | |
| title='Python Basics Video', provider='YouTube', | |
| url='https://youtube.com/p1', | |
| difficulty_level='ADVANCED', duration=30, type='VIDEO', | |
| ) | |
| r1 = auth_client.get('/api/resources/?type=COURSE') | |
| assert r1.data['count'] == 1 and r1.data['results'][0]['id'] == resource.id | |
| r2 = auth_client.get('/api/resources/?difficulty_level=ADVANCED') | |
| assert r2.data['count'] == 1 and r2.data['results'][0]['title'] == 'Python Basics Video' | |
| def test_detail_includes_checkpoints_and_skills(self, auth_client, resource, python_skill): | |
| ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='Intro') | |
| ResourceCheckpoint.objects.create(resource=resource, order_index=2, title='Variables') | |
| response = auth_client.get(f'/api/resources/{resource.id}/') | |
| assert response.status_code == status.HTTP_200_OK | |
| assert len(response.data['checkpoints']) == 2 | |
| assert response.data['checkpoints'][0]['order_index'] == 1 | |
| assert len(response.data['skill_mappings']) == 1 | |
| assert response.data['skill_mappings'][0]['skill']['skill_name'] == 'Python' | |
| assert response.data['skill_mappings'][0]['relevance_score'] == 0.95 | |
| class TestModelConstraints: | |
| def test_url_unique(self, resource): | |
| with pytest.raises(Exception): | |
| Resource.objects.create( | |
| title='Dup', provider='X', url=resource.url, | |
| difficulty_level='BEGINNER', duration=10, type='DOCS', | |
| ) | |
| def test_checkpoint_order_unique_per_resource(self, resource): | |
| ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='A') | |
| with pytest.raises(Exception): | |
| ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='B') | |
| def test_same_order_ok_across_resources(self, python_skill): | |
| r1 = Resource.objects.create(title='R1', provider='P', url='https://a/x', | |
| difficulty_level='BEGINNER', duration=10, type='DOCS') | |
| r2 = Resource.objects.create(title='R2', provider='P', url='https://a/y', | |
| difficulty_level='BEGINNER', duration=10, type='DOCS') | |
| ResourceCheckpoint.objects.create(resource=r1, order_index=1, title='X') | |
| ResourceCheckpoint.objects.create(resource=r2, order_index=1, title='Y') | |
| assert ResourceCheckpoint.objects.count() == 2 | |
| class TestResourceAdminBulkPaste: | |
| def test_bulk_paste_appends_checkpoints(self, resource): | |
| site = AdminSite() | |
| admin = ResourceAdmin(Resource, site) | |
| request = _admin_request() | |
| request.user = User.objects.create_superuser( | |
| username='admin@x.com', email='admin@x.com', password='pw', | |
| ) | |
| form = ResourceAdminForm( | |
| instance=resource, | |
| data={ | |
| 'title': resource.title, 'provider': resource.provider, | |
| 'url': resource.url, 'difficulty_level': resource.difficulty_level, | |
| 'duration': resource.duration, 'type': resource.type, | |
| 'rating': resource.rating, | |
| 'bulk_checkpoints': 'Module 1: Intro\n\nModule 2: Variables\n \nModule 3: Loops', | |
| }, | |
| ) | |
| assert form.is_valid(), form.errors | |
| admin.save_model(request, resource, form, change=True) | |
| cps = list(resource.checkpoints.order_by('order_index')) | |
| assert [c.title for c in cps] == [ | |
| 'Module 1: Intro', 'Module 2: Variables', 'Module 3: Loops', | |
| ] | |
| assert [c.order_index for c in cps] == [1, 2, 3] | |
| assert all(c.source == 'manual' for c in cps) | |
| def test_bulk_paste_refused_when_checkpoints_exist(self, resource): | |
| """Re-pasting onto a resource that already has checkpoints is | |
| rejected — admins must edit the inline rows instead. Prevents | |
| silent duplication and orphaning of UserCheckpointProgress.""" | |
| ResourceCheckpoint.objects.create(resource=resource, order_index=1, title='Existing') | |
| site = AdminSite() | |
| admin = ResourceAdmin(Resource, site) | |
| request = _admin_request() | |
| request.user = User.objects.create_superuser( | |
| username='admin2@x.com', email='admin2@x.com', password='pw', | |
| ) | |
| form = ResourceAdminForm( | |
| instance=resource, | |
| data={ | |
| 'title': resource.title, 'provider': resource.provider, | |
| 'url': resource.url, 'difficulty_level': resource.difficulty_level, | |
| 'duration': resource.duration, 'type': resource.type, | |
| 'rating': resource.rating, | |
| 'bulk_checkpoints': 'New A\nNew B', | |
| }, | |
| ) | |
| assert form.is_valid(), form.errors | |
| admin.save_model(request, resource, form, change=True) | |
| # Only the pre-existing checkpoint remains — the paste was rejected. | |
| orders = list(resource.checkpoints.order_by('order_index').values_list('order_index', 'title')) | |
| assert orders == [(1, 'Existing')] | |
| def test_bulk_paste_empty_noop(self, resource): | |
| site = AdminSite() | |
| admin = ResourceAdmin(Resource, site) | |
| request = _admin_request() | |
| request.user = User.objects.create_superuser( | |
| username='admin3@x.com', email='admin3@x.com', password='pw', | |
| ) | |
| form = ResourceAdminForm( | |
| instance=resource, | |
| data={ | |
| 'title': resource.title, 'provider': resource.provider, | |
| 'url': resource.url, 'difficulty_level': resource.difficulty_level, | |
| 'duration': resource.duration, 'type': resource.type, | |
| 'rating': resource.rating, | |
| 'bulk_checkpoints': ' \n\n ', | |
| }, | |
| ) | |
| assert form.is_valid(), form.errors | |
| admin.save_model(request, resource, form, change=True) | |
| assert resource.checkpoints.count() == 0 | |