Skip to content
This repository has been archived by the owner on Mar 28, 2020. It is now read-only.

Calyx/config cache #130

Merged
merged 6 commits into from
Feb 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions calyx/src/calyx/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@
}
}

# TODO: 本番環境ではRedisかMemcachedにした方が良い
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}

DEFAULT_FROM_EMAIL = os.getenv('CALYX_EMAIL_DEFAULT_FROM', '[email protected]')
EMAIL_ENABLED = os.getenv('CALYX_EMAIL_ENABLED', 'False').lower() == 'true'

Expand Down
19 changes: 19 additions & 0 deletions calyx/src/courses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import TYPE_CHECKING
from django.contrib.auth.models import Group
from django.core.cache import cache
from django.db import models, transaction, IntegrityError
from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -283,3 +284,21 @@ def move_up_ranks(sender, instance: 'Lab', **kwargs):
# 最後の志望の研究室はNULLにする
rank_to_del.lab = None
rank_to_del.save()


@receiver(models.signals.post_save, sender=Config)
def set_config_cache(sender, instance: 'Config', **kwargs):
"""
設定が更新されたときにキャッシュも更新する
:param sender: Modelクラス
:param instance: 設定のインスタンス
:param kwargs:
:return:
"""
config_dict = {
'show_gpa': instance.show_gpa,
'show_username': instance.show_username,
'rank_limit': instance.rank_limit,
}
cache_key = f"course-config-{instance.course_id}"
cache.set(cache_key, config_dict)
48 changes: 48 additions & 0 deletions calyx/src/courses/permissions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import TYPE_CHECKING
from django.core.cache import cache
from rest_framework import permissions

if TYPE_CHECKING:
from typing import Optional
from .models import Course


Expand Down Expand Up @@ -36,3 +38,49 @@ def has_object_permission(self, request, view, obj: 'Course'):
if user.groups.filter(name=obj.admin_group_name).exists():
return True
return False


class GPARequirement(permissions.IsAuthenticated):
"""GPAを入力しているかどうか.表示設定がFalseなら常にTrueが返る"""

message = "GPAが未入力です."

def satisfy_requirements(self, show: bool, obj: 'Optional[str, float]') -> bool:
if show:
if obj is None:
return False
return True

def get_config(self, obj):
return cache.get(f'course-config-{obj.pk}')

def has_object_permission(self, request, view, obj):
user = request.user
config = self.get_config(obj)
return self.satisfy_requirements(config['show_gpa'], user.gpa)


class ScreenNameRequirement(GPARequirement):
"""screen_nameを入力しているかどうか.表示設定がFalseなら常にTrueが返る"""

message = "表示名が未入力です."

def has_object_permission(self, request, view, obj):
user = request.user
config = self.get_config(obj)
return self.satisfy_requirements(config['show_username'], user.screen_name)


class RankSubmitted(GPARequirement):
"""志望を提出しているかどうか"""

message = "研究室の志望順位がまだ提出されていない,または規定の数を満たしていません."

def has_object_permission(self, request, view, obj):
user = request.user
config = self.get_config(obj)
rank_limit = config['rank_limit']
user_submitted = user.rank_set.filter(course=obj).count()
if rank_limit != user_submitted:
return False
return True
25 changes: 21 additions & 4 deletions calyx/src/courses/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
from django.core import exceptions
from django.core.cache import cache
from django.db import IntegrityError, models, transaction
from django.contrib.auth import get_user_model, password_validation
from django.conf import settings
Expand Down Expand Up @@ -36,6 +37,23 @@ class Meta:
"screen_name": {"read_only": True},
}

def _rm_from_represent(self, represent: dict, config_cache: dict, cache_key: str, to_del: str) -> dict:
"""課程の設定のキーと削除対象のキーを受取って存在すれば削除"""
if not config_cache.get(cache_key, True):
if to_del in represent:
represent.pop(to_del)
return represent

def to_representation(self, instance):
"""課程の設定に応じて返すパラメータを決定する"""
represent = super(UserSerializer, self).to_representation(instance)
course = self.context.get('course', None)
if course:
config = cache.get(f"course-config-{course.pk}")
represent = self._rm_from_represent(represent, config, 'show_gpa', 'gpa')
represent = self._rm_from_represent(represent, config, 'show_username', 'screen_name')
return represent


class RankListSerializer(serializers.ListSerializer):
"""希望順位をまとめて作成/更新するシリアライザ"""
Expand All @@ -60,7 +78,7 @@ def create(self, validated_data):
with transaction.atomic():
for i, data in enumerate(validated_data):
rank, _ = Rank.objects.update_or_create(
user=user, course=course, order=i+1, defaults={'lab': data['lab']}
user=user, course=course, order=i, defaults={'lab': data['lab']}
)
rank_list.append(rank)
return rank_list
Expand Down Expand Up @@ -89,11 +107,10 @@ def to_representation(self, data):
:return:
"""
config = self.context['course'].config # type: Config
iterable = data.all() if isinstance(data, models.Manager) else data
rank_per_order = list()
for order in range(1, config.rank_limit+1):
for order in range(config.rank_limit):
rank_per_order.append(
[self.child.to_representation(rank) for rank in iterable.filter(order=order).all()]
[self.child.to_representation(rank) for rank in data.filter(order=order).all()]
)
return rank_per_order

Expand Down
20 changes: 20 additions & 0 deletions calyx/src/courses/tests/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import json
from copy import deepcopy
from collections import OrderedDict
from typing import TYPE_CHECKING
from rest_framework_jwt.settings import api_settings

from courses.models import Lab, Rank

if TYPE_CHECKING:
from courses.models import Course
from typing import List, Dict


years = [2017, 2018, 2019]

Expand Down Expand Up @@ -68,6 +75,19 @@ def to_dict(self, data: OrderedDict) -> dict:
"""OrderedDictを標準のdictに変換する"""
return json.loads(json.dumps(data, ensure_ascii=False))

def create_labs(self, course: 'Course') -> 'List[Lab]':
return [Lab.objects.create(**lab_data, course=course) for lab_data in self.lab_data_set]

def submit_ranks(self, labs: "List[Lab]", user) -> 'List[Rank]':
ranks = []
for i, lab in enumerate(labs):
rank = Rank.objects.create(lab=lab, course=lab.course, user=user, order=i)
ranks.append(rank)
return ranks

def create_rank_order(self, labs: 'List[Lab]') -> 'List[Dict[str, int]]':
return [{'lab': lab.pk for lab in labs}]


class JWTAuthMixin(object):

Expand Down
60 changes: 51 additions & 9 deletions calyx/src/courses/tests/views/test_course_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def setUp(self):
def test_list(self):
"""GET /course/"""
course = Course.objects.create_course(**self.course_data_set[0])
course.join(self.user, self.course_data_set[0]['pin_code'])
expected_json = [
{
"pk": course.pk,
Expand Down Expand Up @@ -74,14 +73,6 @@ def test_retrieve(self):
"""GET /course/<pk>/"""
course_data = self.course_data_set[0]
course = Course.objects.create_course(**course_data)
# ログインしていなければ詳細を閲覧できない
self._unset_credentials()
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(401, resp.status_code)
self._set_credentials()
# 参加していないユーザは詳細を閲覧できない
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(403, resp.status_code)
# 参加しているユーザは閲覧できる
course.join(self.user, course_data['pin_code'])
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
Expand All @@ -102,6 +93,57 @@ def test_retrieve(self):
self.assertEqual(200, resp.status_code)
self.assertEqual(expected_json, self.to_dict(resp.data))

def test_retrieve_permissions(self):
"""GET /courses/<pk>/"""
course_data = self.course_data_set[0]
course = Course.objects.create_course(**course_data)
# ログインしていなければ詳細を閲覧できない
self._unset_credentials()
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(401, resp.status_code)
self._set_credentials()
# 参加していないユーザは詳細を閲覧できない
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(403, resp.status_code)

def test_retrieve_permission_gpa(self):
"""GET /courses/<pk>/"""
course_data = self.course_data_set[0]
course = Course.objects.create_course(**course_data)
# 課程へ加入
course.join(self.user, course_data['pin_code'])
# GPA必須の場合,未入力では閲覧できない
course.config.show_gpa = True
course.config.save()
self.user.gpa = None
self.user.save()
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(403, resp.status_code)
# GPAを入力すれば閲覧できる
self.user.gpa = 2.0
self.user.save()
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(200, resp.status_code)

def test_retrieve_permission_username(self):
"""GET /courses/<pk>/"""
course_data = self.course_data_set[0]
course = Course.objects.create_course(**course_data)
# 課程へ加入
course.join(self.user, course_data['pin_code'])
# screen_name必須の場合,未入力では閲覧できない
course.config.show_username = True
course.config.save()
self.user.screen_name = None
self.user.save()
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(403, resp.status_code)
# GPAを入力すれば閲覧できる
self.user.screen_name = 'test_user'
self.user.save()
resp = self.client.get(f'/courses/{course.pk}/', data={}, format='json')
self.assertEqual(200, resp.status_code)

def test_patch(self):
"""PATCH /courses/<pk>/"""
course_data = self.course_data_set[0]
Expand Down
28 changes: 18 additions & 10 deletions calyx/src/courses/tests/views/test_lab_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,26 @@ def test_retrieve_lab(self):
"""GET /courses/<course_pk>/labs/<lab_pk>/"""
course = self.course
course.join(self.user, self.pin_code)
labs = self.create_labs(course)
ranks = self.submit_ranks(labs, self.user)
resp = self.client.get(f'/courses/{course.pk}/labs/9999/', format='json')
self.assertEqual(404, resp.status_code)
lab = Lab.objects.create(**self.lab_data_set[0], course=course)
expected = {
"pk": lab.pk,
"name": lab.name,
"capacity": lab.capacity,
'rank_set': [[], [], []]
}
resp = self.client.get(f'/courses/{course.pk}/labs/{lab.pk}/', format='json')
self.assertEqual(200, resp.status_code)
self.assertEqual(expected, self.to_dict(resp.data))
for rank in ranks:
lab = rank.lab
expected = {
"pk": lab.pk,
"name": lab.name,
"capacity": lab.capacity,
'rank_set': [[], [], []]
}
expected['rank_set'][rank.order] = [{
'pk': self.user.pk,
'username': self.user.username,
'email': self.user.email
}]
resp = self.client.get(f'/courses/{course.pk}/labs/{lab.pk}/', format='json')
self.assertEqual(200, resp.status_code)
self.assertEqual(expected, self.to_dict(resp.data))

def test_create_lab(self):
"""POST /courses/<course_pk>/labs/"""
Expand Down
18 changes: 15 additions & 3 deletions calyx/src/courses/tests/views/test_ranks_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_create_rank(self):
self.assertEqual(expected, self.to_dict(resp.data))
ranks = Rank.objects.filter(course=self.course, user=self.user).all()
for i, lab in enumerate(self.labs):
self.assertTrue(ranks.filter(lab=lab, order=i+1).exists())
self.assertTrue(ranks.filter(lab=lab, order=i).exists())
# 数が少ない
resp = self.client.post(f'/courses/{self.course.pk}/ranks/', data=expected[1:], format='json')
self.assertEqual(400, resp.status_code)
Expand All @@ -41,6 +41,19 @@ def test_create_rank(self):
resp = self.client.post(f'/courses/9999/ranks/', data=expected, format='json')
self.assertEqual(404, resp.status_code)

def test_update_rank(self):
"""POST /courses/<course_pk>/ranks/"""
self.course.join(self.user, self.pin_code)
self.submit_ranks(self.labs, self.user)
# 逆順にして更新する
expected = [{'lab': lab.pk} for lab in self.labs][::-1]
resp = self.client.post(f'/courses/{self.course.pk}/ranks/', data=expected, format='json')
self.assertEqual(201, resp.status_code)
self.assertEqual(expected, self.to_dict(resp.data))
ranks = Rank.objects.filter(course=self.course, user=self.user).all()
for i, lab in enumerate(self.labs[::-1]):
self.assertTrue(ranks.filter(lab=lab, order=i).exists())

def test_create_rank_permission(self):
"""POST /courses/<course_pk>/ranks/"""
expected = [{'lab': lab.pk} for lab in self.labs]
Expand All @@ -61,7 +74,7 @@ def test_get_ranks(self):
self.assertEqual([], self.to_dict(resp.data))
ranks = [
Rank.objects.create(
course=self.course, user=self.user, lab=lab, order=i+1
course=self.course, user=self.user, lab=lab, order=i
) for i, lab in enumerate(self.labs)
]
expected = [
Expand All @@ -75,7 +88,6 @@ def test_get_ranks(self):
"pk": self.user.pk,
"username": self.user.username,
"email": self.user.email,
"screen_name": self.user.screen_name
}
for i in range(len(ranks)):
expected[i]['rank_set'] = [[], [], []]
Expand Down
11 changes: 8 additions & 3 deletions calyx/src/courses/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
CourseSerializer, CourseWithoutUserSerializer, YearSerializer, PINCodeSerializer,
UserSerializer, ConfigSerializer, LabSerializer, RankSerializer, LabAbstractSerializer
)
from .permissions import IsAdmin, IsCourseMember, IsCourseAdmin
from .permissions import (
IsAdmin, IsCourseMember, IsCourseAdmin, GPARequirement, ScreenNameRequirement, RankSubmitted
)
from .errors import AlreadyJoinedError, NotJoinedError, NotAdminError
from .schemas import CourseJoinSchema, CourseAdminSchema, LabSchema

Expand Down Expand Up @@ -43,7 +45,7 @@ def get_permissions(self):
if self.action == 'list' or self.action == 'create':
self.permission_classes = [permissions.IsAuthenticated]
elif self.action == 'retrieve':
self.permission_classes = [IsCourseMember | IsAdmin]
self.permission_classes = [(IsCourseMember | IsAdmin) & GPARequirement & ScreenNameRequirement]
else:
self.permission_classes = [(IsCourseMember & IsCourseAdmin) | IsAdmin]
return super(CourseViewSet, self).get_permissions()
Expand Down Expand Up @@ -249,8 +251,11 @@ def get_serializer_class(self):
return LabSerializer

def get_permissions(self):
if self.action == "list" or self.action == "retrieve":
if self.action == "list":
self.permission_classes = [IsCourseMember | IsAdmin]
elif self.action == "retrieve":
self.permission_classes = [(IsCourseMember | IsAdmin) & GPARequirement &
ScreenNameRequirement & RankSubmitted]
else:
self.permission_classes = [(IsCourseMember & IsCourseAdmin) | IsAdmin]
return super(LabViewSet, self).get_permissions()
Expand Down
Loading