Skip to content

Commit faea402

Browse files
authored
Merge pull request #26 from DevOps-Cloud-Team5/main
Merging main stuff to side branch
2 parents 4b6f061 + ea86b2e commit faea402

11 files changed

+506
-43
lines changed

.env_example

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
SECRET_KEY=<django_secret>
2+
3+
DB_NAME=<db_name>
4+
DB_HOST=<db_host>
5+
DB_USER=<db_user>
6+
DB_PORT=<db_port>
7+
DB_PASSWORD=<db_password>
8+
9+
AWS_ACCESS_KEY_ID=<aws_access_id>
10+
AWS_SECRET_ACCESS_KEY=<aws_access_key>
11+
AWS_STORAGE_BUCKET_NAME=<aws_bucket_name>
12+
13+
EMAIL_HOST=<email_host>
14+
EMAIL_PORT=<email_port>
15+
EMAIL_USER=<email_user>
16+
EMAIL_PASSWORD=<email_password>
17+
18+
FRONTEND_URL=<frontend_url> # No / after url

api/apps.py

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
class ApiConfig(AppConfig):
55
default_auto_field = 'django.db.models.BigAutoField'
66
name = 'api'
7+
8+
def ready(self):
9+
import api.signals
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.0.2 on 2024-03-17 14:53
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0005_remove_course_schedule'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='attendenceacknowledgement',
15+
name='attended_student',
16+
field=models.BooleanField(default=None, null=True),
17+
),
18+
migrations.AlterField(
19+
model_name='attendenceacknowledgement',
20+
name='attended_teacher',
21+
field=models.BooleanField(default=None, null=True),
22+
),
23+
]

api/models.py

+17-9
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def clean(self):
5050
super().clean()
5151
self.email = self.__class__.objects.normalize_email(self.email)
5252

53-
def get_classes(self):
53+
def get_enrolled_courses(self):
5454
return [uc.course for uc in UserCourse.objects.filter(user__id=self.id)]
5555

5656
class LectureTypes(models.TextChoices):
@@ -68,17 +68,25 @@ def __str__(self):
6868
return str(self.name)
6969

7070
def get_enrolled_students(self):
71-
return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.STUDENT)]
71+
return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.STUDENT, course=self)]
7272

7373
def get_teachers(self):
74-
return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.TEACHER)]
74+
return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.TEACHER, course=self)]
7575

7676
def get_lectures(self):
77-
return [lecture for lecture in CourseLecture.objects.filter(course=self)]
77+
return CourseLecture.objects.filter(course=self)
78+
79+
def get_lectures_week(self, year : int, week : int):
80+
return CourseLecture.objects.filter(course=self, start_time__year=year, start_time__week=week)
7881

7982
def is_user_enrolled(self, user : User):
8083
return bool(UserCourse.objects.filter(user=user, course=self))
8184

85+
def remove_user_from_course(self, user : User):
86+
queryset = UserCourse.objects.filter(user=user, course=self)
87+
if not queryset: return
88+
queryset[0].delete()
89+
8290
def add_user_to_course(self, user: User):
8391
UserCourse.objects.create(user=user, course=self).save()
8492

@@ -99,13 +107,13 @@ class CourseLecture(models.Model):
99107
default=LectureTypes.LECTURE,
100108
)
101109

102-
def set_attendence_user(self, student : User, teacher=False):
110+
def set_attendence_user(self, student : User, attended : bool, teacher=False):
103111
queryset = AttendenceAcknowledgement.objects.filter(lecture=self, student=student)
104112
if not queryset: ack = AttendenceAcknowledgement.objects.create(lecture=self, student=student)
105113
else: ack = queryset[0]
106114

107-
if teacher: ack.attended_teacher = True
108-
else: ack.attended_student = True
115+
if teacher: ack.attended_teacher = attended
116+
else: ack.attended_student = attended
109117
ack.save()
110118

111119
def get_attendence_user(self, student : User):
@@ -117,8 +125,8 @@ def get_attendence(self):
117125
return [] if not queryset else queryset[0]
118126

119127
class AttendenceAcknowledgement(models.Model):
120-
attended_student = models.BooleanField(default=False)
121-
attended_teacher = models.BooleanField(default=False)
128+
attended_student = models.BooleanField(default=None, null=True)
129+
attended_teacher = models.BooleanField(default=None, null=True)
122130
student = models.ForeignKey(User, null=False, related_name='user_ack', on_delete=models.CASCADE)
123131
lecture = models.ForeignKey(CourseLecture, null=False, related_name='lecture_ack', on_delete=models.CASCADE)
124132

api/serializers.py

+64-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
77
from rest_framework.validators import UniqueValidator
88

9-
from .models import AccountRoles, Course
9+
from .models import AccountRoles, Course, CourseLecture, LectureTypes
1010

1111
User = get_user_model()
1212

@@ -24,11 +24,21 @@ class UserSerializer(serializers.ModelSerializer):
2424
class Meta:
2525
model = User
2626
fields = '__all__'
27-
27+
28+
class CourseUserSerializer(serializers.ModelSerializer):
29+
class Meta:
30+
model = User
31+
fields = ["username", "first_name", "last_name", "role"]
32+
2833
class CourseSerializer(serializers.ModelSerializer):
2934
class Meta:
3035
model = Course
3136
fields = '__all__'
37+
38+
class LectureSerializer(serializers.ModelSerializer):
39+
class Meta:
40+
model = CourseLecture
41+
fields = '__all__'
3242

3343
# Defines the rules for registering a new user. Required fields, validation rules etc.
3444
class CreateUserSerializer(serializers.ModelSerializer):
@@ -73,8 +83,6 @@ def create(self, validated_data):
7383
c.save()
7484
return c
7585

76-
77-
7886
class MassEnrollSerializer(serializers.Serializer):
7987
usernames = serializers.ListField(required=True, allow_empty=False, child=serializers.CharField(max_length=150))
8088

@@ -88,4 +96,55 @@ def validate_enroll(self, username):
8896

8997
def validate(self, attrs):
9098
for username in attrs["usernames"]: self.validate_enroll(username)
91-
return attrs
99+
return attrs
100+
101+
class AddLectureSerializer(serializers.Serializer):
102+
MINIMUM_LECTURE_LENGTH = 10 # Minimum lecture length of 10 minutes
103+
104+
start_time = serializers.DateTimeField(required=True)
105+
end_time = serializers.DateTimeField(required=True)
106+
lecture_type = serializers.CharField(required=True)
107+
108+
def validate(self, attrs):
109+
start_time, end_time = attrs["start_time"], attrs["end_time"]
110+
111+
if start_time > end_time:
112+
raise serializers.ValidationError({"error": f"end_time has to be after start_time"})
113+
114+
lecture_length = (end_time - start_time).seconds / 60
115+
if lecture_length < self.MINIMUM_LECTURE_LENGTH:
116+
raise serializers.ValidationError({"error": f"lecture has to be at least {self.MINIMUM_LECTURE_LENGTH} minutes"})
117+
118+
lecture_type = attrs["lecture_type"]
119+
if lecture_type not in LectureTypes.values:
120+
raise serializers.ValidationError({"error": f"invalid lecture type: '{lecture_type}'"})
121+
122+
course : Course = self.context.get("course")
123+
for lecture in course.get_lectures():
124+
if lecture.start_time <= start_time and start_time < lecture.end_time:
125+
raise serializers.ValidationError({"error": "there is already an active lecture during this time range"})
126+
if lecture.start_time < end_time and end_time <= lecture.end_time:
127+
raise serializers.ValidationError({"error": "there is already an active lecture during this time range"})
128+
return attrs
129+
130+
class SetAttendenceTeacherSerializer(serializers.Serializer):
131+
usernames = serializers.DictField(required=True, allow_empty=False, child=
132+
serializers.CharField(max_length=150)
133+
)
134+
135+
def validate_attendence(self, username, attended):
136+
user_query = User.objects.all().filter(username=username)
137+
if (attended := attended.lower()) not in ["true", "false"]: raise serializers.ValidationError({"error": f"invalid attendence state: '{attended}'"})
138+
if not user_query: raise serializers.ValidationError({"error": f"user '{username}' does not exist"})
139+
if user_query[0].role != AccountRoles.STUDENT: raise serializers.ValidationError({"error": f"cannot set the attendence of a non-student: '{username}' is {user_query[0].role}"})
140+
141+
course : Course = self.context.get("course")
142+
if not course.is_user_enrolled(user_query[0]): raise serializers.ValidationError({"error": f"user '{username}' is not enrolled in '{course.course_name}'"})
143+
144+
def validate(self, attrs):
145+
for username, attended in attrs["usernames"].items(): self.validate_attendence(username, attended)
146+
return attrs
147+
148+
class MailTestSerializer(serializers.Serializer):
149+
email = serializers.CharField(required=True)
150+

api/settings.py

+28-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
TEMPLATES = [
2828
{
2929
'BACKEND': 'django.template.backends.django.DjangoTemplates',
30-
'DIRS': [],
30+
'DIRS': [os.path.join(BASE_DIR, 'api', 'templates')],
3131
'APP_DIRS': True,
3232
'OPTIONS': {
3333
'context_processors': [
@@ -81,8 +81,9 @@
8181
'django.contrib.staticfiles',
8282
'corsheaders',
8383
'rest_framework',
84-
'drf_spectacular',
84+
'django_rest_passwordreset',
8585
'rest_framework_simplejwt.token_blacklist',
86+
'drf_spectacular',
8687
"api.apps.ApiConfig"
8788
]
8889

@@ -92,6 +93,15 @@
9293

9394
AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID')
9495

96+
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
97+
EMAIL_HOST = config('EMAIL_HOST')
98+
EMAIL_PORT = config('EMAIL_PORT')
99+
EMAIL_USE_TLS = True
100+
EMAIL_HOST_USER = config('EMAIL_USER')
101+
EMAIL_HOST_PASSWORD = config('EMAIL_PASSWORD')
102+
103+
FRONTEND_URL = config('FRONTEND_URL')
104+
95105
CORS_ALLOW_HEADERS = [
96106
'x-requested-with',
97107
'content-type',
@@ -141,4 +151,19 @@
141151
'rest_framework_simplejwt.authentication.JWTAuthentication',
142152
),
143153
'AUTH_HEADER_TYPES': ('JWT',),
144-
}
154+
}
155+
156+
DJANGO_REST_PASSWORDRESET_TOKEN_CONFIG = {
157+
"CLASS": "django_rest_passwordreset.tokens.RandomStringTokenGenerator",
158+
"OPTIONS": {
159+
"min_length": 20,
160+
"max_length": 30
161+
}
162+
}
163+
DJANGO_REST_MULTITOKENAUTH_RESET_TOKEN_EXPIRY_TIME = 30 # minutes
164+
165+
DJANGO_REST_PASSWORDRESET_EMAIL_TEMPLATES = {
166+
'subject': 'templates/email/password_reset_subject.txt',
167+
'plain_body': 'templates/email/password_reset_email.html',
168+
'html_body': 'templates/email/password_reset_email.html',
169+
}

api/signals.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from django.core.mail import EmailMultiAlternatives
2+
from django.dispatch import receiver
3+
from django.template.loader import render_to_string
4+
from django.urls import reverse
5+
from django.conf import settings
6+
7+
8+
from django_rest_passwordreset.signals import reset_password_token_created
9+
10+
11+
@receiver(reset_password_token_created)
12+
def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs):
13+
"""
14+
Handles password reset tokens
15+
When a token is created, an e-mail needs to be sent to the user
16+
:param sender: View Class that sent the signal
17+
:param instance: View Instance that sent the signal
18+
:param reset_password_token: Token Model Object
19+
:param args:
20+
:param kwargs:
21+
:return:
22+
"""
23+
# send an e-mail to the user
24+
context = {
25+
'current_user': reset_password_token.user,
26+
'username': reset_password_token.user.username,
27+
'email': reset_password_token.user.email,
28+
'reset_password_url': f"{settings.FRONTEND_URL}/reset_password?token={reset_password_token.key}"
29+
}
30+
31+
# render email text
32+
email_html_message = render_to_string('email/password_reset_email.html', context)
33+
email_plaintext_message = render_to_string('email/password_reset_email.txt', context)
34+
35+
msg = EmailMultiAlternatives(
36+
# title:
37+
"Password Reset for Attendunce",
38+
# message:
39+
email_plaintext_message,
40+
# from:
41+
"[email protected]", # Does not matter, it will be overridden
42+
# to:
43+
[reset_password_token.user.email]
44+
)
45+
msg.attach_alternative(email_html_message, "text/html")
46+
msg.send()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<p>Hello,</p>
2+
3+
<p>We received a request to reset the password for your account. If you made this request, please click on the link below or copy and paste it into your browser to complete the process:</p>
4+
5+
<p><a href="{{ reset_password_url }}">{{ reset_password_url }}</a></p>
6+
7+
<p>The link will be valid for the next 30 minutes</p>
8+
9+
<p>If you did not request to reset your password, please ignore this email and your password will remain unchanged.</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Hello,
2+
3+
We received a request to reset the password for your account. If you made this request, please click on the link below or copy and paste it into your browser to complete the process:</p>
4+
5+
{{ reset_password_url }}
6+
7+
The link will be valid for the next 30 minutes
8+
9+
If you did not request to reset your password, please ignore this email and your password will remain unchanged.

0 commit comments

Comments
 (0)