From 55b8506458798b7b7d21be2919ece0105782f276 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Sun, 19 Apr 2020 18:58:15 +0200 Subject: [PATCH 01/12] Create IsStudent permission --- passit/common/permissions.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/passit/common/permissions.py b/passit/common/permissions.py index 4375bb0..17fa9bc 100644 --- a/passit/common/permissions.py +++ b/passit/common/permissions.py @@ -14,3 +14,13 @@ def has_object_permission(self, request, view, obj) -> bool: or hasattr(obj, 'is_owner') and obj.is_owner(request.user.profile) ) )) + + +class IsStudent(BasePermission): + + def has_permission(self, request, view): + return bool( + request.user + and hasattr(request.user, 'profile') + and request.user.profile.memberships.count() + ) From 68e86bbb5ca21513e2c65307168e4b4058da86f3 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Fri, 1 May 2020 18:26:25 +0200 Subject: [PATCH 02/12] Create files app --- conftest.py | 1 + passit/common/permissions.py | 9 +- passit/settings/base.py | 9 ++ passit/subject/models.py | 2 +- passit/subject/serializers.py | 8 +- passit/subject/tests/fixtures.py | 1 + passit/subject/views.py | 3 +- passit/urls.py | 2 + requirements.txt | 1 + teleagh/files/__init__.py | 0 teleagh/files/admin.py | 10 ++ teleagh/files/apps.py | 5 + teleagh/files/factories.py | 12 +++ teleagh/files/managers.py | 13 +++ teleagh/files/migrations/0001_initial.py | 30 ++++++ teleagh/files/migrations/__init__.py | 0 teleagh/files/models.py | 22 +++++ teleagh/files/querysets.py | 9 ++ teleagh/files/serializers.py | 33 +++++++ teleagh/files/tests/__init__.py | 0 teleagh/files/tests/fixtures.py | 35 +++++++ teleagh/files/tests/test_models.py | 9 ++ teleagh/files/tests/test_serializers.py | 22 +++++ teleagh/files/tests/test_views.py | 92 +++++++++++++++++++ teleagh/files/urls.py | 12 +++ teleagh/files/views.py | 14 +++ .../subject/migrations/0011_resource_files.py | 19 ++++ 27 files changed, 367 insertions(+), 6 deletions(-) create mode 100644 teleagh/files/__init__.py create mode 100644 teleagh/files/admin.py create mode 100644 teleagh/files/apps.py create mode 100644 teleagh/files/factories.py create mode 100644 teleagh/files/managers.py create mode 100644 teleagh/files/migrations/0001_initial.py create mode 100644 teleagh/files/migrations/__init__.py create mode 100644 teleagh/files/models.py create mode 100644 teleagh/files/querysets.py create mode 100644 teleagh/files/serializers.py create mode 100644 teleagh/files/tests/__init__.py create mode 100644 teleagh/files/tests/fixtures.py create mode 100644 teleagh/files/tests/test_models.py create mode 100644 teleagh/files/tests/test_serializers.py create mode 100644 teleagh/files/tests/test_views.py create mode 100644 teleagh/files/urls.py create mode 100644 teleagh/files/views.py create mode 100644 teleagh/subject/migrations/0011_resource_files.py diff --git a/conftest.py b/conftest.py index feebc5f..6bba0eb 100644 --- a/conftest.py +++ b/conftest.py @@ -6,6 +6,7 @@ 'passit.news.tests.fixtures', 'passit.subject.tests.fixtures', 'passit.events.tests.fixtures', + 'passit.files.tests.fixtures' ] diff --git a/passit/common/permissions.py b/passit/common/permissions.py index 17fa9bc..eed87e4 100644 --- a/passit/common/permissions.py +++ b/passit/common/permissions.py @@ -22,5 +22,12 @@ def has_permission(self, request, view): return bool( request.user and hasattr(request.user, 'profile') - and request.user.profile.memberships.count() + and request.user.profile.memberships.exists() + ) + + +class IsOwner(BasePermission): + def has_object_permission(self, request, view, obj): + return bool( + hasattr(obj, 'is_owner') and obj.is_owner(request.user.profile) ) diff --git a/passit/settings/base.py b/passit/settings/base.py index 5af06b0..63d9295 100644 --- a/passit/settings/base.py +++ b/passit/settings/base.py @@ -46,6 +46,7 @@ 'webpack_loader', 'django_extensions', 'django_celery_results', + 'easy_thumbnails', # my apps 'passit.accounts.apps.AccountsConfig', 'passit.lecturers.apps.LecturersConfig', @@ -243,3 +244,11 @@ CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", 'redis://localhost:6379/2') CELERY_RESULT_BACKEND = 'django-db' + +# EASY_THUMBNAILS + +THUMBNAIL_ALIASES = { + '': { + 'small': {'size': (400, 0), 'crop': False}, + }, +} diff --git a/passit/subject/models.py b/passit/subject/models.py index e639689..f2e15ba 100644 --- a/passit/subject/models.py +++ b/passit/subject/models.py @@ -76,7 +76,7 @@ class Resource(TimeStampedModel, OwnedModel): description = models.TextField(blank=True) category = models.CharField(max_length=50, choices=ResourceCategoryChoices.choices()) subject = models.ForeignKey('Subject', on_delete=models.CASCADE, related_name='resources') - + files = models.ManyToManyField('files.File', blank=True) objects = ResourceManager.from_queryset(ResourceQuerySet)() def __str__(self) -> str: diff --git a/passit/subject/serializers.py b/passit/subject/serializers.py index cec0e3d..ea6f285 100644 --- a/passit/subject/serializers.py +++ b/passit/subject/serializers.py @@ -8,6 +8,7 @@ from ..accounts.models import UserProfile from ..common.serializers import OwnedModelSerializerMixin +from ..files.serializers import FileSerializer from ..lecturers.models import LecturerOfSubjectOfAgeGroup from ..lecturers.serializers import LecturerOfSubjectOfAgeGroupSerializer from ..subject.models import FieldOfStudy, Subject, Resource, FieldOfStudyOfAgeGroup, SubjectOfAgeGroup @@ -100,8 +101,9 @@ class ResourceBaseSerializer(OwnedModelSerializerMixin, FlexFieldsModelSerialize class Meta: model = Resource - fields = ('id', 'name', 'image', 'url', 'description', 'subject', 'category', 'created_by_profile', - 'modified_by_profile', 'created_by', 'modified_by') + fields = ('id', 'name', 'image', 'url', 'description', 'subject', 'category', 'files', + 'created_by_profile', 'modified_by_profile', 'created_by', 'modified_by') expandable_fields: Dict[str, Tuple[Serializer, Dict[str, Any]]] = { - 'subject': (SubjectBaseSerializer, {'fields': ['id', 'name', 'semester', ]}) + 'subject': (SubjectBaseSerializer, {'fields': ['id', 'name', 'semester', ]}), + 'files': (FileSerializer, {'many': True}) } diff --git a/passit/subject/tests/fixtures.py b/passit/subject/tests/fixtures.py index cf25d15..63cd134 100644 --- a/passit/subject/tests/fixtures.py +++ b/passit/subject/tests/fixtures.py @@ -43,4 +43,5 @@ def resource_data(subject): 'name': 'resource', 'subject': subject.id, 'category': ResourceCategoryChoices.OTHER, + 'files': [], } diff --git a/passit/subject/views.py b/passit/subject/views.py index 8825663..4d6cac3 100644 --- a/passit/subject/views.py +++ b/passit/subject/views.py @@ -49,7 +49,8 @@ def get_queryset(self): class ResourceViewSet(FlexFieldsModelViewSet): serializer_class = ResourceBaseSerializer - queryset = Resource.objects.all() + queryset = Resource.objects.select_related('created_by__user').select_related('modified_by__user').\ + prefetch_related('files') filterset_class = ResourceFilterSet permission_classes = [IsAuthenticated, ] permit_list_expands = ('subject',) diff --git a/passit/urls.py b/passit/urls.py index 9cb5ae6..56efd21 100644 --- a/passit/urls.py +++ b/passit/urls.py @@ -24,6 +24,7 @@ from rest_framework.routers import DefaultRouter from .events.urls import router as events_router +from .files.urls import router as files_router from .lecturers.urls import router as lecturers_router from .news.urls import router as news_router from .subject.urls import router as subject_router @@ -46,6 +47,7 @@ router.registry.extend(lecturers_router.registry) router.registry.extend(news_router.registry) router.registry.extend(events_router.registry) +router.registry.extend(files_router.registry) urlpatterns = [ path('', index), diff --git a/requirements.txt b/requirements.txt index 15063d5..9b1de66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ requests==2.25.1 django-redis==4.12.1 celery==5.0.5 django-celery-results== 2.0.1 +easy-thumbnails==2.7 time-machine==2.0.1 django-admin-display==1.3.0 django-jazzmin==2.4.4 diff --git a/teleagh/files/__init__.py b/teleagh/files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teleagh/files/admin.py b/teleagh/files/admin.py new file mode 100644 index 0000000..74d09bd --- /dev/null +++ b/teleagh/files/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from teleagh.files.models import File + + +class FileAdmin(admin.ModelAdmin): + pass + + +admin.site.register(File, FileAdmin) diff --git a/teleagh/files/apps.py b/teleagh/files/apps.py new file mode 100644 index 0000000..c86f272 --- /dev/null +++ b/teleagh/files/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class FilesConfig(AppConfig): + name = 'files' diff --git a/teleagh/files/factories.py b/teleagh/files/factories.py new file mode 100644 index 0000000..15fac3f --- /dev/null +++ b/teleagh/files/factories.py @@ -0,0 +1,12 @@ +import factory + +from .models import File + + +class FileFactory(factory.DjangoModelFactory): + name = factory.sequence(lambda n: f'Name {n}') + other = factory.django.FileField(filename='file.txt') + image = factory.django.ImageField(filename='image.jpg', width=10, height=10) + + class Meta: + model = File diff --git a/teleagh/files/managers.py b/teleagh/files/managers.py new file mode 100644 index 0000000..3577921 --- /dev/null +++ b/teleagh/files/managers.py @@ -0,0 +1,13 @@ +from typing import TYPE_CHECKING + +from django.db.models import Manager, QuerySet + +from .querysets import FileQuerySet + +if TYPE_CHECKING: + from .models import File + + +class FileManager(Manager): # type: ignore + def get_queryset(self) -> 'QuerySet[File]': + return FileQuerySet(self.model, using=self._db) # type: ignore diff --git a/teleagh/files/migrations/0001_initial.py b/teleagh/files/migrations/0001_initial.py new file mode 100644 index 0000000..ad784aa --- /dev/null +++ b/teleagh/files/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.6 on 2020-04-30 18:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('accounts', '0006_auto_20200314_2220'), + ] + + operations = [ + migrations.CreateModel( + name='File', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=100)), + ('other', models.FileField(blank=True, null=True, upload_to='', verbose_name='files/')), + ('image', models.ImageField(blank=True, null=True, upload_to='', verbose_name='images/')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='file_created', to='accounts.UserProfile')), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='file_modified', to='accounts.UserProfile')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/teleagh/files/migrations/__init__.py b/teleagh/files/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teleagh/files/models.py b/teleagh/files/models.py new file mode 100644 index 0000000..65e8c79 --- /dev/null +++ b/teleagh/files/models.py @@ -0,0 +1,22 @@ +from typing import Dict + +from django.conf import settings +from django.db import models +from easy_thumbnails.fields import ThumbnailerImageField +from easy_thumbnails.files import ThumbnailFile + +from .managers import FileManager +from .querysets import FileQuerySet +from ..common.models import OwnedModel + + +class File(OwnedModel): + name = models.CharField(max_length=100, blank=True) + other = models.FileField('files/', blank=True, null=True) + image = ThumbnailerImageField('images/', blank=True, null=True) + + objects = FileManager.from_queryset(FileQuerySet)() + + def get_thumbnails(self) -> Dict[str, ThumbnailFile]: + thumbnail_names = settings.THUMBNAIL_ALIASES[''].keys() + return {name: self.image[name] for name in thumbnail_names} diff --git a/teleagh/files/querysets.py b/teleagh/files/querysets.py new file mode 100644 index 0000000..f58e849 --- /dev/null +++ b/teleagh/files/querysets.py @@ -0,0 +1,9 @@ +from django.db.models import QuerySet + +from ..accounts.models import UserProfile + + +class FileQuerySet(QuerySet): # type: ignore + + def filter_by_profile(self, profile: 'UserProfile'): + return self.filter(created_by=profile) diff --git a/teleagh/files/serializers.py b/teleagh/files/serializers.py new file mode 100644 index 0000000..78a7eb6 --- /dev/null +++ b/teleagh/files/serializers.py @@ -0,0 +1,33 @@ +from django.utils.translation import gettext as _ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from .models import File +from ..common.serializers import OwnedModelSerializerMixin + + +class FileSerializer(OwnedModelSerializerMixin, serializers.ModelSerializer): + thumbnails = serializers.SerializerMethodField() + + class Meta: + model = File + fields = ('id', 'name', 'other', 'image', 'thumbnails') + + def get_thumbnails(self, obj: File): + request = self.context.get('request') + if obj.image: + thumbnails = obj.get_thumbnails() + return {name: self.build_url(thumbnail, request) for name, thumbnail in thumbnails.items()} + + @staticmethod + def build_url(thumbnail, request=None) -> str: + if request: + return request.build_absolute_uri(thumbnail.url) + return thumbnail.url + + def validate(self, attrs): + if all((attrs.get('other'), attrs.get('image'))): + raise ValidationError(_('Only one of image and other should be chosen')) + elif not any((attrs.get('other'), attrs.get('image'))): + raise ValidationError(_('One of image and other should be chosen')) + return attrs diff --git a/teleagh/files/tests/__init__.py b/teleagh/files/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/teleagh/files/tests/fixtures.py b/teleagh/files/tests/fixtures.py new file mode 100644 index 0000000..beaaadb --- /dev/null +++ b/teleagh/files/tests/fixtures.py @@ -0,0 +1,35 @@ +import pytest +from django.core.files import File +from django.core.files.uploadedfile import SimpleUploadedFile + +from teleagh.files.factories import FileFactory + + +@pytest.fixture +def file(db): + return FileFactory(name='file') + + +@pytest.fixture +def students1_file(student1): + return FileFactory(created_by=student1, modified_by=student1) + + +@pytest.fixture +def file_other_data(): + return { + 'name': 'file', + 'other': SimpleUploadedFile('f.txt', b'text') + } + +@pytest.fixture +def file_image_data(image_file): + return { + 'name': 'image', + 'other': SimpleUploadedFile('image.jpg', image_file.read(), content_type='image/jpg') + } + + +@pytest.fixture +def image_file() -> File: + return FileFactory.build().image.file diff --git a/teleagh/files/tests/test_models.py b/teleagh/files/tests/test_models.py new file mode 100644 index 0000000..b1fd9c7 --- /dev/null +++ b/teleagh/files/tests/test_models.py @@ -0,0 +1,9 @@ +from ..factories import FileFactory +from ..models import File + + +def test_filter_by_profile_queryset_method(user_profile1, user_profile2): + expected = FileFactory.create_batch(2, created_by=user_profile1) + FileFactory.create_batch(2, created_by=user_profile2) + qs = File.objects.filter_by_profile(user_profile1) + assert set(expected) == set(qs) diff --git a/teleagh/files/tests/test_serializers.py b/teleagh/files/tests/test_serializers.py new file mode 100644 index 0000000..4c09191 --- /dev/null +++ b/teleagh/files/tests/test_serializers.py @@ -0,0 +1,22 @@ +import pytest +# --- FileSerializer --- +from django.core.files.uploadedfile import SimpleUploadedFile + +from teleagh.files.serializers import FileSerializer + + +def test_image_or_other_should_be_set(api_rf, file_other_data): + file_other_data.pop('other') + serializer = FileSerializer(data=file_other_data) + serializer.is_valid() + assert 'One of' in serializer.errors['non_field_errors'][0] + + +@pytest.mark.django_db +def test_image_or_other_cant_be_set_together(api_rf, file_other_data, image_file): + file_other_data['image'] = SimpleUploadedFile('a.jpg', content=image_file.read(), + content_type='image/jpg') + serializer = FileSerializer(data=file_other_data) + serializer.is_valid() + assert 'Only one of' in serializer.errors['non_field_errors'][0] + diff --git a/teleagh/files/tests/test_views.py b/teleagh/files/tests/test_views.py new file mode 100644 index 0000000..e227345 --- /dev/null +++ b/teleagh/files/tests/test_views.py @@ -0,0 +1,92 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import force_authenticate + +from teleagh.common.utils import setup_view +from teleagh.files.factories import FileFactory +from teleagh.files.models import File +from teleagh.files.views import FileViewSet + + +@pytest.fixture +def file_list_view(): + return FileViewSet.as_view({'get': 'list', 'post': 'create'}) + + +@pytest.fixture +def file_detail_view(): + return FileViewSet.as_view({'get': 'retrieve', 'post': 'create', 'delete': 'destroy', 'put': 'update'}) + +# --- FileViewSet --- + + +def test_user_without_profile_cant_access_view(api_rf, file_list_view, user1): + request = api_rf.get(reverse('api:file-list')) + force_authenticate(request, user1) + response = file_list_view(request) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_user_without_membership_cant_access_view(api_rf, file_list_view, user_profile1): + request = api_rf.get(reverse('api:file-list')) + force_authenticate(request, user_profile1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_403_FORBIDDEN + + +def test_student_can_access_view(api_rf, file_list_view, student1): + request = api_rf.get(reverse('api:file-list')) + force_authenticate(request, student1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_200_OK + + +def test_files_are_filtered_by_user(api_rf, student1, student2, students1_file): + FileFactory(created_by=student2) + request = api_rf.get(reverse('api:file-list')) + request.user = student1.user + view = setup_view(FileViewSet(), request) + assert list(view.get_queryset()) == list(File.objects.filter_by_profile(student1)) + + +def test_student_can_create_file_with_other(api_rf, file_list_view, student1, file_other_data): + request = api_rf.post(reverse('api:file-list'), data=file_other_data) + force_authenticate(request, student1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert File.objects.count() == 1 + + +def test_student_can_create_file_with_image(api_rf, file_list_view, student1, file_image_data): + request = api_rf.post(reverse('api:file-list'), data=file_image_data) + force_authenticate(request, student1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert File.objects.count() == 1 + + +def test_owner_can_edit_his_files(api_rf, file_detail_view, student1, students1_file, file_other_data): + request = api_rf.put(reverse('api:file-list'), data=file_other_data, args=(students1_file.id,)) + force_authenticate(request, student1.user) + response = file_detail_view(request, pk=students1_file.id) + students1_file.refresh_from_db() + assert response.status_code == status.HTTP_200_OK + assert students1_file.name == 'file' + + +def test_privileged_cant_edit_someones_files(api_rf, file_detail_view, student1, students1_file, file_other_data, + representative_profile): + request = api_rf.put(reverse('api:file-list'), data=file_other_data, args=(students1_file.id,)) + force_authenticate(request, representative_profile.user) + response = file_detail_view(request, pk=students1_file.id) + assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_student_can_delete_his_files(api_rf, file_detail_view, student1, students1_file, file_other_data): + request = api_rf.delete(reverse('api:file-list'), args=(students1_file.id,)) + force_authenticate(request, student1.user) + response = file_detail_view(request, pk=students1_file.id) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not File.objects.filter(id=students1_file.id).exists() + diff --git a/teleagh/files/urls.py b/teleagh/files/urls.py new file mode 100644 index 0000000..1e195c1 --- /dev/null +++ b/teleagh/files/urls.py @@ -0,0 +1,12 @@ +from typing import Any, List + +from rest_framework.routers import DefaultRouter + +from .views import FileViewSet + +router = DefaultRouter() +router.register('files', FileViewSet) + +urlpatterns: List[Any] = [ + +] diff --git a/teleagh/files/views.py b/teleagh/files/views.py new file mode 100644 index 0000000..1c7db4e --- /dev/null +++ b/teleagh/files/views.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets + +from .models import File +from .serializers import FileSerializer +from ..common.permissions import IsStudent, IsOwner + + +class FileViewSet(viewsets.ModelViewSet): + queryset = File.objects.all() + serializer_class = FileSerializer + permission_classes = (IsStudent, IsOwner) + + def get_queryset(self): + return File.objects.filter_by_profile(self.request.user.profile) diff --git a/teleagh/subject/migrations/0011_resource_files.py b/teleagh/subject/migrations/0011_resource_files.py new file mode 100644 index 0000000..92b5025 --- /dev/null +++ b/teleagh/subject/migrations/0011_resource_files.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.6 on 2020-04-30 18:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ('subject', '0010_subject_syllabus_code'), + ] + + operations = [ + migrations.AddField( + model_name='resource', + name='files', + field=models.ManyToManyField(blank=True, to='files.File'), + ), + ] From 101cc133f9b4a0b8ec3723ac6be3da1fb7880ba8 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Fri, 1 May 2020 21:52:56 +0200 Subject: [PATCH 03/12] Alter M2M to use through model --- .../migrations/0012_auto_20200501_1933.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 teleagh/subject/migrations/0012_auto_20200501_1933.py diff --git a/teleagh/subject/migrations/0012_auto_20200501_1933.py b/teleagh/subject/migrations/0012_auto_20200501_1933.py new file mode 100644 index 0000000..e1ce0cc --- /dev/null +++ b/teleagh/subject/migrations/0012_auto_20200501_1933.py @@ -0,0 +1,36 @@ +# Generated by Django 2.2.6 on 2020-05-01 19:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0002_auto_20200501_1933'), + ('subject', '0011_resource_files'), + ] + + operations = [ + migrations.CreateModel( + name='ResourceAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='resource_attachments', to='files.File')), + ], + ), + migrations.RemoveField( + model_name='resource', + name='files', + ), + migrations.AddField( + model_name='resource', + name='files', + field=models.ManyToManyField(blank=True, through='subject.ResourceAttachment', to='files.File'), + ), + migrations.AddField( + model_name='resourceattachment', + name='resource', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subject.Resource'), + ), + ] From 33443adf844d60d8775d0bd86a649d2fbd20a924 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Fri, 1 May 2020 22:20:26 +0200 Subject: [PATCH 04/12] Squash resource migrations --- ...501_1933.py => 0011_auto_20200501_2023.py} | 20 ++++++++----------- .../subject/migrations/0011_resource_files.py | 19 ------------------ 2 files changed, 8 insertions(+), 31 deletions(-) rename teleagh/subject/migrations/{0012_auto_20200501_1933.py => 0011_auto_20200501_2023.py} (72%) delete mode 100644 teleagh/subject/migrations/0011_resource_files.py diff --git a/teleagh/subject/migrations/0012_auto_20200501_1933.py b/teleagh/subject/migrations/0011_auto_20200501_2023.py similarity index 72% rename from teleagh/subject/migrations/0012_auto_20200501_1933.py rename to teleagh/subject/migrations/0011_auto_20200501_2023.py index e1ce0cc..3b9beb4 100644 --- a/teleagh/subject/migrations/0012_auto_20200501_1933.py +++ b/teleagh/subject/migrations/0011_auto_20200501_2023.py @@ -1,36 +1,32 @@ -# Generated by Django 2.2.6 on 2020-05-01 19:33 +# Generated by Django 2.2.6 on 2020-05-01 20:23 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('files', '0002_auto_20200501_1933'), - ('subject', '0011_resource_files'), + ('subject', '0010_subject_syllabus_code'), ] operations = [ + migrations.RemoveField( + model_name='resource', + name='image', + ), migrations.CreateModel( name='ResourceAttachment', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('file', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='resource_attachments', to='files.File')), + ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subject.Resource')), ], ), - migrations.RemoveField( - model_name='resource', - name='files', - ), migrations.AddField( model_name='resource', name='files', field=models.ManyToManyField(blank=True, through='subject.ResourceAttachment', to='files.File'), ), - migrations.AddField( - model_name='resourceattachment', - name='resource', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subject.Resource'), - ), ] diff --git a/teleagh/subject/migrations/0011_resource_files.py b/teleagh/subject/migrations/0011_resource_files.py deleted file mode 100644 index 92b5025..0000000 --- a/teleagh/subject/migrations/0011_resource_files.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.6 on 2020-04-30 18:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('files', '0001_initial'), - ('subject', '0010_subject_syllabus_code'), - ] - - operations = [ - migrations.AddField( - model_name='resource', - name='files', - field=models.ManyToManyField(blank=True, to='files.File'), - ), - ] From 1df93b84ac90705374b7959356863c5ee577553a Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Fri, 1 May 2020 23:06:11 +0200 Subject: [PATCH 05/12] Add migrations to files --- .../migrations/0002_auto_20200501_1933.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 teleagh/files/migrations/0002_auto_20200501_1933.py diff --git a/teleagh/files/migrations/0002_auto_20200501_1933.py b/teleagh/files/migrations/0002_auto_20200501_1933.py new file mode 100644 index 0000000..c88d89e --- /dev/null +++ b/teleagh/files/migrations/0002_auto_20200501_1933.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.6 on 2020-05-01 19:33 + +from django.db import migrations +import easy_thumbnails.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='image', + field=easy_thumbnails.fields.ThumbnailerImageField(blank=True, null=True, upload_to='', verbose_name='images/'), + ), + ] From 9f8bc0601860fe07824cb7cdd32888822d4c62f5 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Fri, 1 May 2020 23:09:23 +0200 Subject: [PATCH 06/12] remove image from resource serializer --- passit/subject/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passit/subject/serializers.py b/passit/subject/serializers.py index ea6f285..7ea5abe 100644 --- a/passit/subject/serializers.py +++ b/passit/subject/serializers.py @@ -101,7 +101,7 @@ class ResourceBaseSerializer(OwnedModelSerializerMixin, FlexFieldsModelSerialize class Meta: model = Resource - fields = ('id', 'name', 'image', 'url', 'description', 'subject', 'category', 'files', + fields = ('id', 'name', 'url', 'description', 'subject', 'category', 'files', 'created_by_profile', 'modified_by_profile', 'created_by', 'modified_by') expandable_fields: Dict[str, Tuple[Serializer, Dict[str, Any]]] = { 'subject': (SubjectBaseSerializer, {'fields': ['id', 'name', 'semester', ]}), From a68ef2ed5566a8a6bdbf1625efcee28a4419d957 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Fri, 1 May 2020 23:13:11 +0200 Subject: [PATCH 07/12] Remove image and add files to resource --- passit/subject/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/passit/subject/models.py b/passit/subject/models.py index f2e15ba..a49a403 100644 --- a/passit/subject/models.py +++ b/passit/subject/models.py @@ -72,17 +72,21 @@ class ResourceCategoryChoices(CustomEnum): class Resource(TimeStampedModel, OwnedModel): name = models.CharField(max_length=100) url = models.URLField(blank=True) - image = models.ImageField(blank=True) description = models.TextField(blank=True) category = models.CharField(max_length=50, choices=ResourceCategoryChoices.choices()) subject = models.ForeignKey('Subject', on_delete=models.CASCADE, related_name='resources') - files = models.ManyToManyField('files.File', blank=True) + files = models.ManyToManyField('files.File', through='ResourceAttachment', blank=True) objects = ResourceManager.from_queryset(ResourceQuerySet)() def __str__(self) -> str: return f'{self.name} - {self.subject}' +class ResourceAttachment(models.Model): + file = models.ForeignKey('files.File', on_delete=models.PROTECT, related_name="resource_attachments") + resource = models.ForeignKey('Resource', on_delete=models.CASCADE) + + class SubjectOfAgeGroup(models.Model): subject = models.ForeignKey(Subject, on_delete=models.CASCADE, related_name='subjects') description = models.TextField(blank=True) From e4224dfde6a35de57ea470b384a67c104200a546 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Sat, 2 May 2020 22:36:34 +0200 Subject: [PATCH 08/12] Add tests for resources --- passit/subject/serializers.py | 11 +++++ passit/subject/tests/fixtures.py | 2 +- teleagh/subject/tests/test_serializers.py | 42 ++++++++++++++++ teleagh/subject/tests/test_views.py | 60 +++++++++++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 teleagh/subject/tests/test_serializers.py create mode 100644 teleagh/subject/tests/test_views.py diff --git a/passit/subject/serializers.py b/passit/subject/serializers.py index 7ea5abe..35bac79 100644 --- a/passit/subject/serializers.py +++ b/passit/subject/serializers.py @@ -8,6 +8,7 @@ from ..accounts.models import UserProfile from ..common.serializers import OwnedModelSerializerMixin +from ..files.models import File from ..files.serializers import FileSerializer from ..lecturers.models import LecturerOfSubjectOfAgeGroup from ..lecturers.serializers import LecturerOfSubjectOfAgeGroupSerializer @@ -24,6 +25,15 @@ def get_queryset(self) -> 'QuerySet[FieldOfStudyOfAgeGroup]': return FieldOfStudyOfAgeGroup.objects.filter_by_profile(profile) +class FileRelatedFile(serializers.PrimaryKeyRelatedField): + def get_queryset(self): + request = self.context.get('request') + profile = request and request.user and request.user.profile + if not profile: + return File.objects.all() + return File.objects.filter_by_profile(profile) + + class FieldAgeGroupDefault: field_age_group = None @@ -98,6 +108,7 @@ class Meta: class ResourceBaseSerializer(OwnedModelSerializerMixin, FlexFieldsModelSerializer): + files = FileRelatedFile(many=True) class Meta: model = Resource diff --git a/passit/subject/tests/fixtures.py b/passit/subject/tests/fixtures.py index 63cd134..599ca86 100644 --- a/passit/subject/tests/fixtures.py +++ b/passit/subject/tests/fixtures.py @@ -38,7 +38,7 @@ def field_age_group_data(field_of_study): @pytest.fixture -def resource_data(subject): +def resource_data_without_files(subject): return { 'name': 'resource', 'subject': subject.id, diff --git a/teleagh/subject/tests/test_serializers.py b/teleagh/subject/tests/test_serializers.py new file mode 100644 index 0000000..25e6bd1 --- /dev/null +++ b/teleagh/subject/tests/test_serializers.py @@ -0,0 +1,42 @@ +from unittest import mock + +from ..serializers import FieldOfStudyOfAgeGroupSerializer, ResourceBaseSerializer + + +# --- FieldOfAgeGroupSerializer --- + + +def test_can_serialize_field_age_group(field_age_group, field_age_group_data): + serializer = FieldOfStudyOfAgeGroupSerializer(field_age_group) + assert serializer.data == { + 'id': field_age_group.id, + 'field_of_study': field_age_group.field_of_study.id, + 'students_start_year': 2018 + } + + +def test_can_create_field_age_group(db, field_age_group_data): + serializer = FieldOfStudyOfAgeGroupSerializer(data=field_age_group_data) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert (instance.field_of_study.id, instance.students_start_year)\ + == (field_age_group_data['field_of_study'], field_age_group_data['students_start_year']) + + +# --- ResourceSerializer --- + +def test_resource_owned_model_serializer(resource_data_without_files, api_rf, user_profile1, user_profile2): + request_user1 = mock.Mock() + request_user1.user = user_profile1.user + request_user2 = mock.Mock() + request_user2.user = user_profile2.user + serializer = ResourceBaseSerializer(data=resource_data_without_files, context={'request': request_user1}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == user_profile1, "Creator is set on instace" + assert instance.modified_by == user_profile1, "Modifier is set on instance" + serializer = ResourceBaseSerializer(data=resource_data_without_files, instance=instance, context={'request': request_user2}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == user_profile1, "Creator is unchanged on instance" + assert instance.modified_by == user_profile2, "Modifier is changed on instance" diff --git a/teleagh/subject/tests/test_views.py b/teleagh/subject/tests/test_views.py new file mode 100644 index 0000000..4f20da0 --- /dev/null +++ b/teleagh/subject/tests/test_views.py @@ -0,0 +1,60 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import force_authenticate + +from teleagh.files.factories import FileFactory +from teleagh.files.models import File +from teleagh.subject.models import Resource +from teleagh.subject.views import ResourceViewSet + + +@pytest.fixture +def resource_list_view(): + return ResourceViewSet.as_view({'get': 'list', 'post': 'create'}) + + +@pytest.fixture +def resource_detail_view(): + return ResourceViewSet.as_view({'get': 'retrieve', 'post': 'create', 'delete': 'destroy', 'put': 'update'}) + +# ---ResourceViewSet--- + + +def test_student_can_create_resource_without_files(api_rf, resource_list_view, student1, resource_data_without_files): + request = api_rf.post(reverse('api:resource-list'), data=resource_data_without_files) + force_authenticate(request, student1.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert Resource.objects.count() == 1 + + +def test_student_can_create_resource_with_files(api_rf, resource_list_view, student1, resource_data_without_files): + file = FileFactory(created_by=student1) + resource_data_with_files = resource_data_without_files.copy() + resource_data_with_files['files'] = [file.id] + request = api_rf.post(reverse('api:resource-list'), data=resource_data_with_files) + force_authenticate(request, student1.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert Resource.objects.count() == 1 + assert list(Resource.objects.first().files.values('id')) == [{'id': file.id}] + + +def test_student_can_create_resource_using_own_files(api_rf, resource_list_view, student1, students1_file, + resource_data_without_files): + resource_data_without_files['files'] = [students1_file.id] + request = api_rf.post(reverse('api:file-list'), data=resource_data_without_files) + force_authenticate(request, student1.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert File.objects.count() == 1 + +def test_student_cant_create_resource_using_someones_files(api_rf, resource_list_view, student2, students1_file, + resource_data_without_files): + resource_data_without_files['files'] = [students1_file.id] + request = api_rf.post(reverse('api:file-list'), data=resource_data_without_files) + force_authenticate(request, student2.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "Invalid pk" in response.data['files'][0] From e9de314938a8eab55b3e62fcd6be5cf36d669281 Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Sun, 3 May 2020 17:48:45 +0200 Subject: [PATCH 09/12] Add tests for resources --- teleagh/subject/tests/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/teleagh/subject/tests/test_views.py b/teleagh/subject/tests/test_views.py index 4f20da0..b34b152 100644 --- a/teleagh/subject/tests/test_views.py +++ b/teleagh/subject/tests/test_views.py @@ -50,6 +50,7 @@ def test_student_can_create_resource_using_own_files(api_rf, resource_list_view, assert response.status_code == status.HTTP_201_CREATED assert File.objects.count() == 1 + def test_student_cant_create_resource_using_someones_files(api_rf, resource_list_view, student2, students1_file, resource_data_without_files): resource_data_without_files['files'] = [students1_file.id] From 84d0a0283183478efdbbf3d1ab75e8346ff0eb9e Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Sun, 3 May 2020 18:20:09 +0200 Subject: [PATCH 10/12] Fix file serializer to be owned --- teleagh/files/serializers.py | 3 ++- teleagh/files/tests/test_serializers.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/teleagh/files/serializers.py b/teleagh/files/serializers.py index 78a7eb6..fcece4d 100644 --- a/teleagh/files/serializers.py +++ b/teleagh/files/serializers.py @@ -11,7 +11,8 @@ class FileSerializer(OwnedModelSerializerMixin, serializers.ModelSerializer): class Meta: model = File - fields = ('id', 'name', 'other', 'image', 'thumbnails') + fields = ('id', 'name', 'other', 'image', 'thumbnails', 'created_by_profile', + 'modified_by_profile', 'created_by', 'modified_by',) def get_thumbnails(self, obj: File): request = self.context.get('request') diff --git a/teleagh/files/tests/test_serializers.py b/teleagh/files/tests/test_serializers.py index 4c09191..5c1d209 100644 --- a/teleagh/files/tests/test_serializers.py +++ b/teleagh/files/tests/test_serializers.py @@ -1,3 +1,5 @@ +from unittest import mock + import pytest # --- FileSerializer --- from django.core.files.uploadedfile import SimpleUploadedFile @@ -20,3 +22,20 @@ def test_image_or_other_cant_be_set_together(api_rf, file_other_data, image_file serializer.is_valid() assert 'Only one of' in serializer.errors['non_field_errors'][0] + +def test_created_by_is_set_on_instance(api_rf, student1, file_other_data): + request = mock.Mock(user=student1.user) + serializer = FileSerializer(data=file_other_data, context={'request': request}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == student1 and instance.modified_by == student1, \ + "Creator and modifier are set on instance" + + +def test_modified_by_is_set_on_instance(api_rf, student1, student2, file_other_data, students1_file): + request = mock.Mock(user=student2.user) + serializer = FileSerializer(data=file_other_data, instance=students1_file, context={'request': request}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == student1 and instance.modified_by == student2, \ + "Creator is the same and modifier is set as another student" From 850b04c0d141caa7845d3d9b8c86c57ef9e4052d Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Tue, 6 Oct 2020 15:57:43 +0200 Subject: [PATCH 11/12] PASSIT-10003 Change production STATIC and MEDIA root --- passit/settings/production.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/passit/settings/production.py b/passit/settings/production.py index 46c584d..15bf330 100644 --- a/passit/settings/production.py +++ b/passit/settings/production.py @@ -49,5 +49,5 @@ db_from_env = dj_database_url.config(default=DATABASE_URL, conn_max_age=500, ssl_require=True) DATABASES['default'].update(db_from_env) -STATIC_ROOT = os.path.join(BASE_DIR, 'public') -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +STATIC_ROOT = Path(BASE_DIR).parent.joinpath('public') +MEDIA_ROOT = Path(BASE_DIR).parent.joinpath('media') From 5d2c98d9d5afe1c02c9f64aa98f1cd85bc807bfc Mon Sep 17 00:00:00 2001 From: Szymon Cader Date: Sun, 11 Oct 2020 21:01:54 +0200 Subject: [PATCH 12/12] Refactor tests so they are class based --- passit/common/tests/test_models.py | 4 - passit/news/tests/test_serializers.py | 133 +++++++++++----------- teleagh/accounts/tests/test_models.py | 16 +++ teleagh/accounts/tests/test_views.py | 28 +++++ teleagh/events/tests/test_views.py | 35 ++++++ teleagh/files/tests/test_models.py | 12 +- teleagh/files/tests/test_serializers.py | 64 +++++------ teleagh/files/tests/test_views.py | 132 ++++++++++----------- teleagh/lecturers/tests/test_models.py | 14 +++ teleagh/subject/tests/test_serializers.py | 72 ++++++------ teleagh/subject/tests/test_views.py | 79 +++++++------ 11 files changed, 331 insertions(+), 258 deletions(-) create mode 100644 teleagh/accounts/tests/test_models.py create mode 100644 teleagh/accounts/tests/test_views.py create mode 100644 teleagh/events/tests/test_views.py create mode 100644 teleagh/lecturers/tests/test_models.py diff --git a/passit/common/tests/test_models.py b/passit/common/tests/test_models.py index df3bb87..1099a87 100644 --- a/passit/common/tests/test_models.py +++ b/passit/common/tests/test_models.py @@ -1,7 +1,3 @@ import pytest from unittest import mock - - -def test_empty(): - assert True \ No newline at end of file diff --git a/passit/news/tests/test_serializers.py b/passit/news/tests/test_serializers.py index 19abe3f..bc39afc 100644 --- a/passit/news/tests/test_serializers.py +++ b/passit/news/tests/test_serializers.py @@ -5,77 +5,74 @@ # --- NewsSerializer --- +class TestNewsSerializer: -def test_serializer_have_correct_fields(): - assert set(NewsSerializer().fields) == {'id', 'title', 'content', 'subject_group', 'field_age_group', 'attachment', - 'created_by', 'modified_by', 'created_by_profile', 'modified_by_profile', - 'created_at', 'updated_at', 'is_owner'} + def test_serializer_have_correct_fields(self): + assert set(NewsSerializer().fields) == {'id', 'title', 'content', 'subject_group', 'field_age_group', 'attachment', + 'created_by', 'modified_by', 'created_by_profile', 'modified_by_profile', + 'created_at', 'updated_at', 'is_owner'} + def test_serializer_serializes_news(self, news, user_profile1, user_profile2): + request = mock.Mock() + request.user = user_profile1.user + news.created_by = user_profile1 + news.modified_by = user_profile2 + data = NewsSerializer(news, context={'request': request}) + expected_data = { + 'id': news.id, + 'title': 'New timetable', + 'content': '', + 'subject_group': news.subject_group_id, + 'field_age_group': news.subject_group.field_age_group_id, + 'created_by': user_profile1.get_name(), + 'modified_by': user_profile2.get_name(), + 'created_at': data.data['created_at'], + 'updated_at': data.data['updated_at'], + 'attachment': None, + 'is_owner': True + } -def test_serializer_serializes_news(news, user_profile1, user_profile2): - request = mock.Mock() - request.user = user_profile1.user - news.created_by = user_profile1 - news.modified_by = user_profile2 - data = NewsSerializer(news, context={'request': request}) - expected_data = { - 'id': news.id, - 'title': 'New timetable', - 'content': '', - 'subject_group': news.subject_group_id, - 'field_age_group': news.subject_group.field_age_group_id, - 'created_by': user_profile1.get_name(), - 'modified_by': user_profile2.get_name(), - 'created_at': data.data['created_at'], - 'updated_at': data.data['updated_at'], - 'attachment': None, - 'is_owner': True - } + assert data.data == expected_data - assert data.data == expected_data + def test_serializer_can_create_news(self, student1, subject_group): + request = mock.Mock() + request.user = student1.user + data = { + 'title': 'New timetable', + 'content': 'not blank', + 'subject_group': subject_group.id, + 'field_age_group': subject_group.field_age_group.id + } + news = NewsSerializer(data=data, context={'request': request}) + news.is_valid(raise_exception=True) + news.save() + assert (news.instance.title, news.instance.subject_group_id, news.instance.field_age_group_id) ==\ + (data['title'], data['subject_group'], subject_group.field_age_group_id) + def test_content_cant_be_empty(self, subject_group): + data = { + 'id': 1, + 'title': 'New timetable', + 'content': '', + 'subject_group': subject_group.id, + 'field_age_group': subject_group.field_age_group.id + } + serializer = NewsSerializer(data=data) + serializer.is_valid() + assert set(serializer.errors.keys()) == {'content',} -def test_serializer_can_create_news(student1, subject_group): - request = mock.Mock() - request.user = student1.user - data = { - 'title': 'New timetable', - 'content': 'not blank', - 'subject_group': subject_group.id, - 'field_age_group': subject_group.field_age_group.id - } - news = NewsSerializer(data=data, context={'request': request}) - news.is_valid(raise_exception=True) - news.save() - assert (news.instance.title, news.instance.subject_group_id, news.instance.field_age_group_id) ==\ - (data['title'], data['subject_group'], subject_group.field_age_group_id) - - -def test_content_cant_be_empty(subject_group): - data = { - 'id': 1, - 'title': 'New timetable', - 'content': '', - 'subject_group': subject_group.id, - 'field_age_group': subject_group.field_age_group.id - } - serializer = NewsSerializer(data=data) - serializer.is_valid() - assert set(serializer.errors.keys()) == {'content',} - - -def test_news_owned_model_serializer(news_data, api_rf, student1, student2): - request_user1 = mock.Mock() - request_user1.user = student1.user - request_user2 = mock.Mock() - request_user2.user = student2.user - serializer = NewsSerializer(data=news_data, context={'request': request_user1}) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - assert instance.created_by == student1, "Creator is set on instace" - assert instance.modified_by == student1, "Modifier is set on instance" - serializer = NewsSerializer(data=news_data, instance=instance, context={'request': request_user2}) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - assert instance.created_by == student1, "Creator is unchanged on instance" - assert instance.modified_by == student2, "Modifier is changed on instance" + def test_news_owned_model_serializer(self, news_data, api_rf, student1, student2): + request_user1 = mock.Mock() + request_user1.user = student1.user + request_user2 = mock.Mock() + request_user2.user = student2.user + serializer = NewsSerializer(data=news_data, context={'request': request_user1}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == student1, "Creator is set on instace" + assert instance.modified_by == student1, "Modifier is set on instance" + serializer = NewsSerializer(data=news_data, instance=instance, context={'request': request_user2}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == student1, "Creator is unchanged on instance" + assert instance.modified_by == student2, "Modifier is changed on instance" diff --git a/teleagh/accounts/tests/test_models.py b/teleagh/accounts/tests/test_models.py new file mode 100644 index 0000000..77892be --- /dev/null +++ b/teleagh/accounts/tests/test_models.py @@ -0,0 +1,16 @@ +from ..factories import MembershipFactory +from ..models import Membership + + +class TestMembershipQuerySet: + + def test_filter_by_profile_method(self, user_profile1, user_profile2): + expected = MembershipFactory.create_batch(2, profile=user_profile1) + MembershipFactory(profile=user_profile2) + assert set(Membership.objects.filter_by_profile(user_profile1)) == set(expected) + + def test_get_default_by_profile(self, user_profile1, user_profile2): + expected = MembershipFactory(profile=user_profile1, is_default=True) + MembershipFactory(profile=user_profile1, is_default=False) + MembershipFactory(profile=user_profile2, is_default=True) + assert Membership.objects.get_default_by_profile(user_profile1) == expected diff --git a/teleagh/accounts/tests/test_views.py b/teleagh/accounts/tests/test_views.py new file mode 100644 index 0000000..49a44ea --- /dev/null +++ b/teleagh/accounts/tests/test_views.py @@ -0,0 +1,28 @@ +import pytest +from django.urls import reverse +from rest_framework.test import force_authenticate + +from ..factories import MembershipFactory +from ..models import Membership +from ..views import CustomUserViewSet + + +@pytest.fixture +def custom_user_list_view(): + return CustomUserViewSet.as_view({'put': 'set_default_field_age_group', 'post': 'create'}) + + +class TestCustomUserViewSet: + + def test_set_default_field_age_group_action(self, student1, api_rf, custom_user_list_view): + default_membership = student1.memberships.get() + + non_default_membership: Membership = MembershipFactory(profile=student1, is_default=False) + request = api_rf.put(reverse('accounts:users-set-default-field-age-group'), + data={'field_age_group': non_default_membership.field_age_group_id}) + force_authenticate(request, student1.user) + custom_user_list_view(request) + non_default_membership.refresh_from_db() + default_membership.refresh_from_db() + assert non_default_membership.is_default + assert not default_membership.is_default diff --git a/teleagh/events/tests/test_views.py b/teleagh/events/tests/test_views.py new file mode 100644 index 0000000..146e0fb --- /dev/null +++ b/teleagh/events/tests/test_views.py @@ -0,0 +1,35 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import force_authenticate + +from ..views import EventViewSet + + +@pytest.fixture +def event_list_view(): + return EventViewSet.as_view({'get': 'list', 'post': 'create'}) + + +class TestEventViewSet: + + def test_can_create_event_without_subject_group(self, event_data, api_rf, event_list_view, user_profile1): + request = api_rf.post(reverse('api:event-list'), data=event_data) + force_authenticate(request, user_profile1.user) + response = event_list_view(request) + assert not event_data.get('subject_group') + assert response.status_code == status.HTTP_201_CREATED + + def test_can_create_event_with_subject_group(self, event_with_subject_group_data, api_rf, event_list_view, user_profile1): + request = api_rf.post(reverse('api:event-list'), data=event_with_subject_group_data) + force_authenticate(request, user_profile1.user) + response = event_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + + def test_cant_create_event_without_field_age_group(self, event_data, api_rf, event_list_view, user_profile1): + event_data.pop('field_age_group') + request = api_rf.post(reverse('api:event-list'), data=event_data) + force_authenticate(request, user_profile1.user) + response = event_list_view(request) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data.get('field_age_group'), 'field_age_group is required' diff --git a/teleagh/files/tests/test_models.py b/teleagh/files/tests/test_models.py index b1fd9c7..4f4f893 100644 --- a/teleagh/files/tests/test_models.py +++ b/teleagh/files/tests/test_models.py @@ -2,8 +2,10 @@ from ..models import File -def test_filter_by_profile_queryset_method(user_profile1, user_profile2): - expected = FileFactory.create_batch(2, created_by=user_profile1) - FileFactory.create_batch(2, created_by=user_profile2) - qs = File.objects.filter_by_profile(user_profile1) - assert set(expected) == set(qs) +class TestFileQuerySet: + + def test_filter_by_profile_queryset_method(self, user_profile1, user_profile2): + expected = FileFactory.create_batch(2, created_by=user_profile1) + FileFactory.create_batch(2, created_by=user_profile2) + qs = File.objects.filter_by_profile(user_profile1) + assert set(expected) == set(qs) diff --git a/teleagh/files/tests/test_serializers.py b/teleagh/files/tests/test_serializers.py index 5c1d209..228d81d 100644 --- a/teleagh/files/tests/test_serializers.py +++ b/teleagh/files/tests/test_serializers.py @@ -1,41 +1,39 @@ from unittest import mock import pytest -# --- FileSerializer --- from django.core.files.uploadedfile import SimpleUploadedFile from teleagh.files.serializers import FileSerializer -def test_image_or_other_should_be_set(api_rf, file_other_data): - file_other_data.pop('other') - serializer = FileSerializer(data=file_other_data) - serializer.is_valid() - assert 'One of' in serializer.errors['non_field_errors'][0] - - -@pytest.mark.django_db -def test_image_or_other_cant_be_set_together(api_rf, file_other_data, image_file): - file_other_data['image'] = SimpleUploadedFile('a.jpg', content=image_file.read(), - content_type='image/jpg') - serializer = FileSerializer(data=file_other_data) - serializer.is_valid() - assert 'Only one of' in serializer.errors['non_field_errors'][0] - - -def test_created_by_is_set_on_instance(api_rf, student1, file_other_data): - request = mock.Mock(user=student1.user) - serializer = FileSerializer(data=file_other_data, context={'request': request}) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - assert instance.created_by == student1 and instance.modified_by == student1, \ - "Creator and modifier are set on instance" - - -def test_modified_by_is_set_on_instance(api_rf, student1, student2, file_other_data, students1_file): - request = mock.Mock(user=student2.user) - serializer = FileSerializer(data=file_other_data, instance=students1_file, context={'request': request}) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - assert instance.created_by == student1 and instance.modified_by == student2, \ - "Creator is the same and modifier is set as another student" +class TestFileSerializer: + + def test_image_or_other_should_be_set(self, api_rf, file_other_data): + file_other_data.pop('other') + serializer = FileSerializer(data=file_other_data) + serializer.is_valid() + assert 'One of' in serializer.errors['non_field_errors'][0] + + @pytest.mark.django_db + def test_image_or_other_cant_be_set_together(self, api_rf, file_other_data, image_file): + file_other_data['image'] = SimpleUploadedFile('a.jpg', content=image_file.read(), + content_type='image/jpg') + serializer = FileSerializer(data=file_other_data) + serializer.is_valid() + assert 'Only one of' in serializer.errors['non_field_errors'][0] + + def test_created_by_is_set_on_instance(self, api_rf, student1, file_other_data): + request = mock.Mock(user=student1.user) + serializer = FileSerializer(data=file_other_data, context={'request': request}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == student1 and instance.modified_by == student1, \ + "Creator and modifier are set on instance" + + def test_modified_by_is_set_on_instance(self, api_rf, student1, student2, file_other_data, students1_file): + request = mock.Mock(user=student2.user) + serializer = FileSerializer(data=file_other_data, instance=students1_file, context={'request': request}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == student1 and instance.modified_by == student2, \ + "Creator is the same and modifier is set as another student" diff --git a/teleagh/files/tests/test_views.py b/teleagh/files/tests/test_views.py index e227345..75303da 100644 --- a/teleagh/files/tests/test_views.py +++ b/teleagh/files/tests/test_views.py @@ -18,75 +18,67 @@ def file_list_view(): def file_detail_view(): return FileViewSet.as_view({'get': 'retrieve', 'post': 'create', 'delete': 'destroy', 'put': 'update'}) -# --- FileViewSet --- - -def test_user_without_profile_cant_access_view(api_rf, file_list_view, user1): - request = api_rf.get(reverse('api:file-list')) - force_authenticate(request, user1) - response = file_list_view(request) - assert response.status_code == status.HTTP_403_FORBIDDEN - - -def test_user_without_membership_cant_access_view(api_rf, file_list_view, user_profile1): - request = api_rf.get(reverse('api:file-list')) - force_authenticate(request, user_profile1.user) - response = file_list_view(request) - assert response.status_code == status.HTTP_403_FORBIDDEN - - -def test_student_can_access_view(api_rf, file_list_view, student1): - request = api_rf.get(reverse('api:file-list')) - force_authenticate(request, student1.user) - response = file_list_view(request) - assert response.status_code == status.HTTP_200_OK - - -def test_files_are_filtered_by_user(api_rf, student1, student2, students1_file): - FileFactory(created_by=student2) - request = api_rf.get(reverse('api:file-list')) - request.user = student1.user - view = setup_view(FileViewSet(), request) - assert list(view.get_queryset()) == list(File.objects.filter_by_profile(student1)) - - -def test_student_can_create_file_with_other(api_rf, file_list_view, student1, file_other_data): - request = api_rf.post(reverse('api:file-list'), data=file_other_data) - force_authenticate(request, student1.user) - response = file_list_view(request) - assert response.status_code == status.HTTP_201_CREATED - assert File.objects.count() == 1 - - -def test_student_can_create_file_with_image(api_rf, file_list_view, student1, file_image_data): - request = api_rf.post(reverse('api:file-list'), data=file_image_data) - force_authenticate(request, student1.user) - response = file_list_view(request) - assert response.status_code == status.HTTP_201_CREATED - assert File.objects.count() == 1 - - -def test_owner_can_edit_his_files(api_rf, file_detail_view, student1, students1_file, file_other_data): - request = api_rf.put(reverse('api:file-list'), data=file_other_data, args=(students1_file.id,)) - force_authenticate(request, student1.user) - response = file_detail_view(request, pk=students1_file.id) - students1_file.refresh_from_db() - assert response.status_code == status.HTTP_200_OK - assert students1_file.name == 'file' - - -def test_privileged_cant_edit_someones_files(api_rf, file_detail_view, student1, students1_file, file_other_data, - representative_profile): - request = api_rf.put(reverse('api:file-list'), data=file_other_data, args=(students1_file.id,)) - force_authenticate(request, representative_profile.user) - response = file_detail_view(request, pk=students1_file.id) - assert response.status_code == status.HTTP_404_NOT_FOUND - - -def test_student_can_delete_his_files(api_rf, file_detail_view, student1, students1_file, file_other_data): - request = api_rf.delete(reverse('api:file-list'), args=(students1_file.id,)) - force_authenticate(request, student1.user) - response = file_detail_view(request, pk=students1_file.id) - assert response.status_code == status.HTTP_204_NO_CONTENT - assert not File.objects.filter(id=students1_file.id).exists() +class TestFileViewSet: + + def test_user_without_profile_cant_access_view(self, api_rf, file_list_view, user1): + request = api_rf.get(reverse('api:file-list')) + force_authenticate(request, user1) + response = file_list_view(request) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_user_without_membership_cant_access_view(self, api_rf, file_list_view, user_profile1): + request = api_rf.get(reverse('api:file-list')) + force_authenticate(request, user_profile1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_student_can_access_view(self, api_rf, file_list_view, student1): + request = api_rf.get(reverse('api:file-list')) + force_authenticate(request, student1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_200_OK + + def test_files_are_filtered_by_user(self, api_rf, student1, student2, students1_file): + FileFactory(created_by=student2) + request = api_rf.get(reverse('api:file-list')) + request.user = student1.user + view = setup_view(FileViewSet(), request) + assert list(view.get_queryset()) == list(File.objects.filter_by_profile(student1)) + + def test_student_can_create_file_with_other(self, api_rf, file_list_view, student1, file_other_data): + request = api_rf.post(reverse('api:file-list'), data=file_other_data) + force_authenticate(request, student1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert File.objects.count() == 1 + + def test_student_can_create_file_with_image(self, api_rf, file_list_view, student1, file_image_data): + request = api_rf.post(reverse('api:file-list'), data=file_image_data) + force_authenticate(request, student1.user) + response = file_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert File.objects.count() == 1 + + def test_owner_can_edit_his_files(self, api_rf, file_detail_view, student1, students1_file, file_other_data): + request = api_rf.put(reverse('api:file-list'), data=file_other_data, args=(students1_file.id,)) + force_authenticate(request, student1.user) + response = file_detail_view(request, pk=students1_file.id) + students1_file.refresh_from_db() + assert response.status_code == status.HTTP_200_OK + assert students1_file.name == 'file' + + def test_privileged_cant_edit_someones_files(self, api_rf, file_detail_view, student1, students1_file, file_other_data, + representative_profile): + request = api_rf.put(reverse('api:file-list'), data=file_other_data, args=(students1_file.id,)) + force_authenticate(request, representative_profile.user) + response = file_detail_view(request, pk=students1_file.id) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_student_can_delete_his_files(self, api_rf, file_detail_view, student1, students1_file, file_other_data): + request = api_rf.delete(reverse('api:file-list'), args=(students1_file.id,)) + force_authenticate(request, student1.user) + response = file_detail_view(request, pk=students1_file.id) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not File.objects.filter(id=students1_file.id).exists() diff --git a/teleagh/lecturers/tests/test_models.py b/teleagh/lecturers/tests/test_models.py new file mode 100644 index 0000000..112d692 --- /dev/null +++ b/teleagh/lecturers/tests/test_models.py @@ -0,0 +1,14 @@ +import pytest + +from teleagh.lecturers.factories import LecturerOfSubjectOfAgeGroupFactory +from teleagh.lecturers.models import LecturerOfSubjectOfAgeGroup + + +class TestLecturerOfSubjectQuerySet: + + @pytest.mark.django_db + def test_annotate_students_start_year(self): + expected_students_start_year = LecturerOfSubjectOfAgeGroupFactory().subject_group.field_age_group.\ + students_start_year + lecturer_of_age_group = LecturerOfSubjectOfAgeGroup.objects.annotate_students_start_year().get() + assert lecturer_of_age_group.students_start_year == expected_students_start_year diff --git a/teleagh/subject/tests/test_serializers.py b/teleagh/subject/tests/test_serializers.py index 25e6bd1..ab1de4b 100644 --- a/teleagh/subject/tests/test_serializers.py +++ b/teleagh/subject/tests/test_serializers.py @@ -3,40 +3,38 @@ from ..serializers import FieldOfStudyOfAgeGroupSerializer, ResourceBaseSerializer -# --- FieldOfAgeGroupSerializer --- - - -def test_can_serialize_field_age_group(field_age_group, field_age_group_data): - serializer = FieldOfStudyOfAgeGroupSerializer(field_age_group) - assert serializer.data == { - 'id': field_age_group.id, - 'field_of_study': field_age_group.field_of_study.id, - 'students_start_year': 2018 - } - - -def test_can_create_field_age_group(db, field_age_group_data): - serializer = FieldOfStudyOfAgeGroupSerializer(data=field_age_group_data) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - assert (instance.field_of_study.id, instance.students_start_year)\ - == (field_age_group_data['field_of_study'], field_age_group_data['students_start_year']) - - -# --- ResourceSerializer --- - -def test_resource_owned_model_serializer(resource_data_without_files, api_rf, user_profile1, user_profile2): - request_user1 = mock.Mock() - request_user1.user = user_profile1.user - request_user2 = mock.Mock() - request_user2.user = user_profile2.user - serializer = ResourceBaseSerializer(data=resource_data_without_files, context={'request': request_user1}) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - assert instance.created_by == user_profile1, "Creator is set on instace" - assert instance.modified_by == user_profile1, "Modifier is set on instance" - serializer = ResourceBaseSerializer(data=resource_data_without_files, instance=instance, context={'request': request_user2}) - serializer.is_valid(raise_exception=True) - instance = serializer.save() - assert instance.created_by == user_profile1, "Creator is unchanged on instance" - assert instance.modified_by == user_profile2, "Modifier is changed on instance" +class TestFieldOfAgeGroupSerializer: + + def test_can_serialize_field_age_group(self, field_age_group, field_age_group_data): + serializer = FieldOfStudyOfAgeGroupSerializer(field_age_group) + assert serializer.data == { + 'id': field_age_group.id, + 'field_of_study': field_age_group.field_of_study.id, + 'students_start_year': 2018 + } + + def test_can_create_field_age_group(self, db, field_age_group_data): + serializer = FieldOfStudyOfAgeGroupSerializer(data=field_age_group_data) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert (instance.field_of_study.id, instance.students_start_year)\ + == (field_age_group_data['field_of_study'], field_age_group_data['students_start_year']) + + +class TestResourceSerializer: + + def test_resource_owned_model_serializer(self, resource_data_without_files, api_rf, user_profile1, user_profile2): + request_user1 = mock.Mock() + request_user1.user = user_profile1.user + request_user2 = mock.Mock() + request_user2.user = user_profile2.user + serializer = ResourceBaseSerializer(data=resource_data_without_files, context={'request': request_user1}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == user_profile1, "Creator is set on instace" + assert instance.modified_by == user_profile1, "Modifier is set on instance" + serializer = ResourceBaseSerializer(data=resource_data_without_files, instance=instance, context={'request': request_user2}) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + assert instance.created_by == user_profile1, "Creator is unchanged on instance" + assert instance.modified_by == user_profile2, "Modifier is changed on instance" diff --git a/teleagh/subject/tests/test_views.py b/teleagh/subject/tests/test_views.py index b34b152..daefb86 100644 --- a/teleagh/subject/tests/test_views.py +++ b/teleagh/subject/tests/test_views.py @@ -18,44 +18,41 @@ def resource_list_view(): def resource_detail_view(): return ResourceViewSet.as_view({'get': 'retrieve', 'post': 'create', 'delete': 'destroy', 'put': 'update'}) -# ---ResourceViewSet--- - - -def test_student_can_create_resource_without_files(api_rf, resource_list_view, student1, resource_data_without_files): - request = api_rf.post(reverse('api:resource-list'), data=resource_data_without_files) - force_authenticate(request, student1.user) - response = resource_list_view(request) - assert response.status_code == status.HTTP_201_CREATED - assert Resource.objects.count() == 1 - - -def test_student_can_create_resource_with_files(api_rf, resource_list_view, student1, resource_data_without_files): - file = FileFactory(created_by=student1) - resource_data_with_files = resource_data_without_files.copy() - resource_data_with_files['files'] = [file.id] - request = api_rf.post(reverse('api:resource-list'), data=resource_data_with_files) - force_authenticate(request, student1.user) - response = resource_list_view(request) - assert response.status_code == status.HTTP_201_CREATED - assert Resource.objects.count() == 1 - assert list(Resource.objects.first().files.values('id')) == [{'id': file.id}] - - -def test_student_can_create_resource_using_own_files(api_rf, resource_list_view, student1, students1_file, - resource_data_without_files): - resource_data_without_files['files'] = [students1_file.id] - request = api_rf.post(reverse('api:file-list'), data=resource_data_without_files) - force_authenticate(request, student1.user) - response = resource_list_view(request) - assert response.status_code == status.HTTP_201_CREATED - assert File.objects.count() == 1 - - -def test_student_cant_create_resource_using_someones_files(api_rf, resource_list_view, student2, students1_file, - resource_data_without_files): - resource_data_without_files['files'] = [students1_file.id] - request = api_rf.post(reverse('api:file-list'), data=resource_data_without_files) - force_authenticate(request, student2.user) - response = resource_list_view(request) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert "Invalid pk" in response.data['files'][0] + +class TestResourceViewSet: + + def test_student_can_create_resource_without_files(self, api_rf, resource_list_view, student1, resource_data_without_files): + request = api_rf.post(reverse('api:resource-list'), data=resource_data_without_files) + force_authenticate(request, student1.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert Resource.objects.count() == 1 + + def test_student_can_create_resource_with_files(self, api_rf, resource_list_view, student1, resource_data_without_files): + file = FileFactory(created_by=student1) + resource_data_with_files = resource_data_without_files.copy() + resource_data_with_files['files'] = [file.id] + request = api_rf.post(reverse('api:resource-list'), data=resource_data_with_files) + force_authenticate(request, student1.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert Resource.objects.count() == 1 + assert list(Resource.objects.first().files.values('id')) == [{'id': file.id}] + + def test_student_can_create_resource_using_own_files(self, api_rf, resource_list_view, student1, students1_file, + resource_data_without_files): + resource_data_without_files['files'] = [students1_file.id] + request = api_rf.post(reverse('api:file-list'), data=resource_data_without_files) + force_authenticate(request, student1.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_201_CREATED + assert File.objects.count() == 1 + + def test_student_cant_create_resource_using_someones_files(self, api_rf, resource_list_view, student2, students1_file, + resource_data_without_files): + resource_data_without_files['files'] = [students1_file.id] + request = api_rf.post(reverse('api:file-list'), data=resource_data_without_files) + force_authenticate(request, student2.user) + response = resource_list_view(request) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "Invalid pk" in response.data['files'][0]