Skip to content
This repository was archived by the owner on Jul 27, 2024. It is now read-only.

Commit 768d4b7

Browse files
committed
refactoring of Take and Student, add CoursePlan
- A student is no longer associated to courses directly. Instead, he can define one or more course-plans, which can also be shared(made public) - Implement object level auth for course plans - only the owner can access it, unless the plan was made public, then it can be read by all users(as well as unauthenticated ones), but it cannot be modified by them. - Add tests for the new course plan serializer, as well as CoursePlan views and permissions - Student.year_in_studies is now calculated based on his chosen track year* - Student.remaining and Student.trickle down are temporarily disabled* * Perhaps these calculations should be moved to be someone else's responsibility, either into CoursePlan or a new API?
1 parent a7eac21 commit 768d4b7

11 files changed

Lines changed: 495 additions & 84 deletions

File tree

Closure_Project/rest_api/admin.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
# Register your models here.
44
from django.contrib import admin
5-
from .models import Course, Student, CourseGroup, Track, Take
5+
from .models import Course, Student, CourseGroup, Track, Take, CoursePlan
66
admin.site.register(Course)
77
admin.site.register(Student)
88
admin.site.register(CourseGroup)
99
admin.site.register(Track)
1010
admin.site.register(Take)
11-
12-
11+
admin.site.register(CoursePlan)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 3.2.4 on 2021-08-28 19:10
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('rest_api', '0004_auto_20210629_0256'),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name='student',
16+
name='courses',
17+
),
18+
migrations.RemoveField(
19+
model_name='student',
20+
name='year_in_studies',
21+
),
22+
migrations.RemoveField(
23+
model_name='take',
24+
name='student',
25+
),
26+
migrations.CreateModel(
27+
name='CoursePlan',
28+
fields=[
29+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
30+
('name', models.TextField(max_length=50, null=True)),
31+
('public', models.BooleanField(default=False)),
32+
('created_at', models.DateTimeField(auto_now_add=True)),
33+
('modified_at', models.DateTimeField(auto_now=True)),
34+
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rest_api.student')),
35+
],
36+
),
37+
migrations.AddField(
38+
model_name='take',
39+
name='course_plan',
40+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rest_api.courseplan'),
41+
preserve_default=False,
42+
),
43+
]

Closure_Project/rest_api/models.py

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Create your models here.
22
import uuid
33
from enum import Enum
4-
from typing import List
4+
from typing import List, Optional
55

66
from django.core.exceptions import ValidationError
77

@@ -205,77 +205,87 @@ def __str__(self):
205205
class Student(models.Model):
206206
user = models.OneToOneField(User, on_delete=models.CASCADE)
207207
track = models.ForeignKey(Track, on_delete=models.CASCADE, null=True)
208-
year_in_studies = models.IntegerField(choices=Year.choices, null=True)
209-
courses = models.ManyToManyField(Course, through='Take', blank=True)
208+
# courses = models.ManyToManyField(Course, through='Take', blank=True)
210209

211210
def __str__(self):
212211
return ', '.join((self.user.username,
213212
self.user.get_full_name(),
214213
f'year={self.year_in_studies}',
215214
f'track={self.track.track_number}' if self.track else 'לא הוגדר מסלול',
216-
f'took {len(self.courses.all())} courses'))
215+
f'has {len(self.courseplan_set.all())} course plans'))
217216

218217
@property
219-
def remaining(self):
220-
track = self.track
221-
groups = track.coursegroup_set.all()
222-
required_by_type = {k: set() for k in REQUIRED_COURSE_TYPES}
223-
required_courses = set()
218+
def year_in_studies(self) -> Optional[int]:
219+
return self.track.data_year if self.track else None
224220

225-
for group in groups:
226-
group_courses = list(group.courses.all())
221+
# @property
222+
# def remaining(self):
223+
# track = self.track
224+
# groups = track.coursegroup_set.all()
225+
# required_by_type = {k: set() for k in REQUIRED_COURSE_TYPES}
226+
# required_courses = set()
227227

228-
required_by_type[group.course_type].update(group_courses)
229-
required_courses.update(group_courses)
228+
# for group in groups:
229+
# group_courses = list(group.courses.all())
230230

231-
done = {t: 0 for t in ALL_COURSE_TYPES}
232-
counted = set()
231+
# required_by_type[group.course_type].update(group_courses)
232+
# required_courses.update(group_courses)
233233

234-
for take in self.take_set.all():
235-
course = take.course
236-
if course not in counted:
237-
counted.add(course)
238-
done[take.type] += course.points
234+
# done = {t: 0 for t in ALL_COURSE_TYPES}
235+
# counted = set()
239236

240-
result = {CourseType.MUST.name: {'required': track.points_must,
241-
'done': done[CourseType.MUST]},
237+
# for take in self.take_set.all():
238+
# course = take.course
239+
# if course not in counted:
240+
# counted.add(course)
241+
# done[take.type] += course.points
242242

243-
CourseType.FROM_LIST.name: {'required': track.points_from_list,
244-
'done': done[CourseType.FROM_LIST]},
243+
# result = {CourseType.MUST.name: {'required': track.points_must,
244+
# 'done': done[CourseType.MUST]},
245245

246-
CourseType.CHOICE.name: {'required': track.points_choice,
247-
'done': done[CourseType.CHOICE]},
246+
# CourseType.FROM_LIST.name: {'required': track.points_from_list,
247+
# 'done': done[CourseType.FROM_LIST]},
248248

249-
CourseType.CORNER_STONE.name: {'required': track.points_corner_stones,
250-
'done': done[CourseType.CORNER_STONE]},
249+
# CourseType.CHOICE.name: {'required': track.points_choice,
250+
# 'done': done[CourseType.CHOICE]},
251251

252-
CourseType.SUPPLEMENTARY.name: {'required': track.points_complementary,
253-
'done': done[CourseType.SUPPLEMENTARY]}}
252+
# CourseType.CORNER_STONE.name: {'required': track.points_corner_stones,
253+
# 'done': done[CourseType.CORNER_STONE]},
254254

255-
def trickle_down(trickle_from: CourseType, trickle_to: CourseType) -> None:
256-
"""
257-
moves extra points between groups, for example, a student taking too many
258-
FROM_LIST courses will have those points counted as CHOICE instead.
255+
# CourseType.SUPPLEMENTARY.name: {'required': track.points_complementary,
256+
# 'done': done[CourseType.SUPPLEMENTARY]}}
259257

260-
:param trickle_from: category from which points are moved
261-
:param trickle_to:category to which points are moved
262-
:return: None
263-
"""
264-
extra = result[trickle_from.name]['done'] - result[trickle_from.name]['required']
265-
if extra > 0:
266-
result[trickle_from.name]['done'] -= extra
267-
result[trickle_to.name]['done'] += extra
258+
# def trickle_down(trickle_from: CourseType, trickle_to: CourseType) -> None:
259+
# """
260+
# moves extra points between groups, for example, a student taking too many
261+
# FROM_LIST courses will have those points counted as CHOICE instead.
268262

269-
trickle_down(CourseType.MUST, CourseType.CHOICE)
270-
trickle_down(CourseType.FROM_LIST, CourseType.CHOICE)
271-
trickle_down(CourseType.CHOICE, CourseType.SUPPLEMENTARY)
272-
trickle_down(CourseType.CORNER_STONE, CourseType.SUPPLEMENTARY)
263+
# :param trickle_from: category from which points are moved
264+
# :param trickle_to:category to which points are moved
265+
# :return: None
266+
# """
267+
# extra = result[trickle_from.name]['done'] - result[trickle_from.name]['required']
268+
# if extra > 0:
269+
# result[trickle_from.name]['done'] -= extra
270+
# result[trickle_to.name]['done'] += extra
273271

274-
return result
272+
# trickle_down(CourseType.MUST, CourseType.CHOICE)
273+
# trickle_down(CourseType.FROM_LIST, CourseType.CHOICE)
274+
# trickle_down(CourseType.CHOICE, CourseType.SUPPLEMENTARY)
275+
# trickle_down(CourseType.CORNER_STONE, CourseType.SUPPLEMENTARY)
276+
277+
# return result
278+
279+
class CoursePlan(models.Model):
280+
owner = models.ForeignKey(Student, on_delete=models.CASCADE)
281+
name = models.TextField(null=True, max_length=50)
282+
public = models.BooleanField(default=False)
283+
created_at = models.DateTimeField(auto_now_add=True)
284+
modified_at = models.DateTimeField(auto_now=True)
275285

276286

277287
class Take(models.Model):
278-
student = models.ForeignKey(Student, on_delete=models.CASCADE)
288+
course_plan = models.ForeignKey(CoursePlan, on_delete=models.CASCADE)
279289
course = models.ForeignKey(Course, on_delete=models.CASCADE)
280290
year_in_studies = models.IntegerField(choices=Year.choices)
281291
semester = models.TextField(choices=Semester.choices)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from rest_framework import permissions
2+
3+
from rest_api.models import CoursePlan
4+
from rest_framework.request import HttpRequest
5+
6+
7+
def request_is_authenticated(request: HttpRequest) -> bool:
8+
return bool(request.user and request.user.is_authenticated)
9+
10+
class CoursePlanPermission(permissions.BasePermission):
11+
""" Allows anyone to read the course plan if it is public.
12+
The owner of the course plan can do anything with it. """
13+
14+
def has_permission(self, request, view):
15+
if request.method in permissions.SAFE_METHODS:
16+
return True
17+
return request_is_authenticated(request)
18+
19+
def has_object_permission(self, request: HttpRequest, view, obj: CoursePlan):
20+
is_authenticated = request_is_authenticated(request)
21+
belongs_to_requester = is_authenticated and obj.owner.user == request.user
22+
if request.method in permissions.SAFE_METHODS:
23+
return belongs_to_requester or obj.public
24+
25+
return belongs_to_requester
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from rest_api.serializers.DynamicSerializer import DynamicFieldsModelSerializer, serializers
2+
from rest_api.models import Take, CoursePlan
3+
4+
5+
class TakeSerializer(DynamicFieldsModelSerializer):
6+
type = serializers.StringRelatedField(read_only=True)
7+
8+
class Meta:
9+
model = Take
10+
fields = ('course', 'course_plan_id', 'year_in_studies', 'semester', 'type')
11+
12+
class CoursePlanSerializer(DynamicFieldsModelSerializer):
13+
takes = TakeSerializer(many=True, source="take_set", partial=True)
14+
owner = serializers.ReadOnlyField(source="owner.id")
15+
16+
class Meta:
17+
model = CoursePlan
18+
fields = ('id', 'owner', 'name', 'public', 'created_at', 'modified_at', 'takes')
19+
read_only_fields = ('id', 'owner', 'created_at', 'modified_at')
20+
21+
22+
def create(self, validated_data):
23+
takes = validated_data.pop("take_set")
24+
25+
plan = CoursePlan.objects.create(**validated_data)
26+
if takes:
27+
for take in takes:
28+
Take.objects.create(course_plan=plan, **take)
29+
return plan
30+
31+
def update(self, instance, validated_data):
32+
takes = validated_data.pop("take_set", None)
33+
34+
CoursePlan.objects.filter(id=instance.id).update(**validated_data)
35+
if takes:
36+
Take.objects.filter(course_plan=instance).delete()
37+
for take in takes:
38+
Take.objects.create(course_plan=instance, **take)
39+
return CoursePlan.objects.get(id=instance.id)
Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,36 @@
11
from django.contrib.auth.models import User
22

3-
from rest_api.serializers.DynamicSerializer import *
3+
from rest_api.serializers.DynamicSerializer import serializers, DynamicFieldsModelSerializer
44
from rest_api.models import Student, Take, Track
5-
from .TakeSerializer import TakeSerializer
5+
from .CoursePlanSerializer import CoursePlanSerializer
66
from .TrackSerializer import TrackSerializer
77

88
from django.shortcuts import get_object_or_404
99

1010

1111
class StudentSerializer(DynamicFieldsModelSerializer):
12-
courses = TakeSerializer(source='take_set', many=True)
1312
pk = serializers.PrimaryKeyRelatedField(source='id',
1413
read_only=True)
1514
username = serializers.CharField(source='user.username', read_only=True)
1615
track_pk = serializers.PrimaryKeyRelatedField(source='track',
1716
queryset=Track.objects.all())
1817
track = TrackSerializer(fields=('track_number', 'name'), read_only=True)
19-
remaining = serializers.JSONField(read_only=True)
18+
course_plans = CoursePlanSerializer(source='courseplan_set', many=True, read_only=True)
2019

2120
class Meta:
2221
model = Student
23-
fields = ('pk', 'username', 'track_pk', 'track', 'year_in_studies', 'remaining', 'courses')
22+
fields = ('pk', 'username', 'track_pk', 'track', 'course_plans', 'year_in_studies')
2423

25-
def update(self, student: Student, validated_data):
26-
take_set = validated_data.pop('take_set')
27-
student.track = validated_data.get('track', student.track)
28-
student.year_in_studies = validated_data.get('year_in_studies', student.year_in_studies)
29-
student.courses.clear()
30-
for take in take_set:
31-
Take.objects.create(student=student,
32-
course=take['course'],
33-
year_in_studies=take['year_in_studies'],
34-
semester=take['semester'])
24+
# def update(self, student: Student, validated_data):
25+
# take_set = validated_data.pop('take_set')
26+
# student.track = validated_data.get('track', student.track)
27+
# student.year_in_studies = validated_data.get('year_in_studies', student.year_in_studies)
28+
# student.courses.clear()
29+
# for take in take_set:
30+
# Take.objects.create(student=student,
31+
# course=take['course'],
32+
# year_in_studies=take['year_in_studies'],
33+
# semester=take['semester'])
3534

36-
student.save()
37-
return student
35+
# student.save()
36+
# return student
Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
1-
from rest_api.serializers.DynamicSerializer import *
2-
from rest_api.models import Course, Take
3-
from .CourseSerializer import CourseSerializer
1+
from rest_api.serializers.DynamicSerializer import DynamicFieldsModelSerializer, serializers
2+
from rest_api.models import Take, CoursePlan
43

54

65
class TakeSerializer(DynamicFieldsModelSerializer):
7-
course = CourseSerializer(fields=('id', 'course_id', 'name', 'semester', 'points'), read_only=True)
8-
pk = serializers.PrimaryKeyRelatedField(source='course', queryset=Course.objects.all())
96
type = serializers.StringRelatedField(read_only=True)
107

118
class Meta:
129
model = Take
13-
fields = ('pk', 'course', 'year_in_studies', 'semester', 'type')
10+
fields = ('course', 'course_plan_id', 'year_in_studies', 'semester', 'type')
11+
12+
class CoursePlanSerializer(DynamicFieldsModelSerializer):
13+
takes = TakeSerializer(many=True, source="take_set", partial=True)
14+
owner = serializers.ReadOnlyField(source="owner.id")
15+
16+
class Meta:
17+
model = CoursePlan
18+
fields = ('id', 'owner', 'name', 'public', 'created_at', 'modified_at', 'takes')
19+
read_only_fields = ('id', 'owner', 'created_at', 'modified_at')
20+
21+
22+
def create(self, validated_data):
23+
takes = validated_data.pop("take_set")
24+
25+
plan = CoursePlan.objects.create(**validated_data)
26+
if takes:
27+
for take in takes:
28+
Take.objects.create(course_plan=plan, **take)
29+
return plan
30+
31+
def update(self, instance, validated_data):
32+
takes = validated_data.pop("take_set", None)
33+
34+
CoursePlan.objects.filter(id=instance.id).update(**validated_data)
35+
if takes:
36+
Take.objects.filter(course_plan=instance).delete()
37+
for take in takes:
38+
Take.objects.create(course_plan=instance, **take)
39+
return CoursePlan.objects.get(id=instance.id)

Closure_Project/rest_api/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
router.register(r'student/me', views.StudentMeViewSet, basename='Student')
1919
router.register(r'track_courses', views.MyTrackCourses, basename='Course')
20+
router.register(r'course_plans', views.CoursePlanViewSet, basename='CoursePlans')
2021

2122
schema_view = get_schema_view(
2223
openapi.Info(

0 commit comments

Comments
 (0)