Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple attachments #42

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'passit.news.tests.fixtures',
'passit.subject.tests.fixtures',
'passit.events.tests.fixtures',
'passit.files.tests.fixtures'
]


Expand Down
17 changes: 17 additions & 0 deletions passit/common/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,20 @@ 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.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)
)
4 changes: 0 additions & 4 deletions passit/common/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import pytest

from unittest import mock


def test_empty():
assert True
133 changes: 65 additions & 68 deletions passit/news/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 9 additions & 0 deletions passit/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
'webpack_loader',
'django_extensions',
'django_celery_results',
'easy_thumbnails',
# my apps
'passit.accounts.apps.AccountsConfig',
'passit.lecturers.apps.LecturersConfig',
Expand Down Expand Up @@ -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},
},
}
4 changes: 2 additions & 2 deletions passit/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
8 changes: 6 additions & 2 deletions passit/subject/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', 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)
Expand Down
19 changes: 16 additions & 3 deletions passit/subject/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

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
from ..subject.models import FieldOfStudy, Subject, Resource, FieldOfStudyOfAgeGroup, SubjectOfAgeGroup
Expand All @@ -23,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

Expand Down Expand Up @@ -97,11 +108,13 @@ class Meta:


class ResourceBaseSerializer(OwnedModelSerializerMixin, FlexFieldsModelSerializer):
files = FileRelatedFile(many=True)

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', '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})
}
3 changes: 2 additions & 1 deletion passit/subject/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ 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,
'category': ResourceCategoryChoices.OTHER,
'files': [],
}
3 changes: 2 additions & 1 deletion passit/subject/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',)
Expand Down
2 changes: 2 additions & 0 deletions passit/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 16 additions & 0 deletions teleagh/accounts/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions teleagh/accounts/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -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
Loading