From 88d838c5d83a6168fd75350ec7584e9a9c13f443 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sat, 1 Jul 2023 11:40:49 +0800 Subject: [PATCH 01/16] Installed Django channels and Daphne --- stude/config/asgi.py | 18 ++++++++---------- stude/config/settings.py | 4 ++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/stude/config/asgi.py b/stude/config/asgi.py index 1b71627..771a0e5 100644 --- a/stude/config/asgi.py +++ b/stude/config/asgi.py @@ -1,16 +1,14 @@ -""" -ASGI config for stude project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ -""" - import os +from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +# Initialize Django ASGI application early to ensure the AppRegistry +# is populated before importing code that may import ORM models. +django_asgi_app = get_asgi_application() -application = get_asgi_application() +application = ProtocolTypeRouter({ + "http": django_asgi_app, + # Just HTTP for now. (We can add other protocols later.) +}) diff --git a/stude/config/settings.py b/stude/config/settings.py index 4a89979..1ce5de9 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -52,6 +52,8 @@ else: # Application definition INSTALLED_APPS = [ + 'daphne', + 'channels', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -88,6 +90,8 @@ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" ROOT_URLCONF = 'config.urls' +ASGI_APPLICATION = "config.asgi.application" + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', From fcd941a80fbfbfcac9a809159e4431610d44dd7d Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sat, 1 Jul 2023 12:01:57 +0800 Subject: [PATCH 02/16] Switch from Django channels to Django rest channels --- stude/config/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/stude/config/settings.py b/stude/config/settings.py index 1ce5de9..cd1dc15 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -53,7 +53,6 @@ else: INSTALLED_APPS = [ 'daphne', - 'channels', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', From 4b20812021e9c8ab477254b88cf4eb65f9a8e79d Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sat, 1 Jul 2023 15:47:28 +0800 Subject: [PATCH 03/16] Added websocket endpoint for student statuses --- stude/api/routing.py | 8 +++++ stude/config/asgi.py | 10 ++++++- stude/student_status/consumers.py | 49 +++++++++++++++++++++++++++++++ stude/student_status/routing.py | 7 +++++ stude/student_status/urls.py | 2 +- 5 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 stude/api/routing.py create mode 100644 stude/student_status/consumers.py create mode 100644 stude/student_status/routing.py diff --git a/stude/api/routing.py b/stude/api/routing.py new file mode 100644 index 0000000..2ecbbbd --- /dev/null +++ b/stude/api/routing.py @@ -0,0 +1,8 @@ +from django.urls import re_path +from channels.routing import URLRouter +import student_status.routing +import student_status.consumers +websocket_urlpatterns = [ + re_path(r'student_status/', + URLRouter(student_status.routing.websocket_urlpatterns)) +] diff --git a/stude/config/asgi.py b/stude/config/asgi.py index 771a0e5..41094d5 100644 --- a/stude/config/asgi.py +++ b/stude/config/asgi.py @@ -1,7 +1,10 @@ import os -from channels.routing import ProtocolTypeRouter +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack from django.core.asgi import get_asgi_application +import api.routing +from django.urls import re_path os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') # Initialize Django ASGI application early to ensure the AppRegistry @@ -11,4 +14,9 @@ django_asgi_app = get_asgi_application() application = ProtocolTypeRouter({ "http": django_asgi_app, # Just HTTP for now. (We can add other protocols later.) + 'websocket': AuthMiddlewareStack( + URLRouter( + [re_path(r'ws/', URLRouter(api.routing.websocket_urlpatterns))] + ) + ), }) diff --git a/stude/student_status/consumers.py b/stude/student_status/consumers.py new file mode 100644 index 0000000..5c3affe --- /dev/null +++ b/stude/student_status/consumers.py @@ -0,0 +1,49 @@ +# consumers.py +import json +from .models import StudentStatus +from .serializers import StudentStatusSerializer +from djangochannelsrestframework.generics import GenericAsyncAPIConsumer +from djangochannelsrestframework.decorators import action +from djangochannelsrestframework.observer import model_observer, observer +from channels.db import database_sync_to_async +import asyncio +from djangochannelsrestframework.mixins import ( + ListModelMixin, + RetrieveModelMixin, +) + + +class StudentStatusConsumer( + ListModelMixin, + RetrieveModelMixin, + GenericAsyncAPIConsumer, +): + + queryset = StudentStatus.objects.all() + serializer_class = StudentStatusSerializer + + async def websocket_connect(self, message): + # This method is called when the websocket is handshaking as part of the connection process. + await self.accept() + self.send_updates_task = asyncio.create_task(self.send_updates()) + + async def websocket_disconnect(self, message): + # This method is called when the WebSocket closes for any reason. + # Here we want to cancel our periodic task that sends updates + self.send_updates_task.cancel() + + @database_sync_to_async + def get_student_statuses(self): + queryset = self.get_queryset() + return StudentStatusSerializer(queryset, many=True).data + + async def send_updates(self): + while True: + try: + data = await self.get_student_statuses() + print(f"Sending update: {data}") # existing debug statement + await self.send(text_data=json.dumps(data)) + await asyncio.sleep(0.5) + except Exception as e: + print(f"Exception in send_updates: {e}") + break # Break the loop on error diff --git a/stude/student_status/routing.py b/stude/student_status/routing.py new file mode 100644 index 0000000..34c10f4 --- /dev/null +++ b/stude/student_status/routing.py @@ -0,0 +1,7 @@ +# routing.py +from django.urls import re_path +from . import consumers + +websocket_urlpatterns = [ + re_path(r"active", consumers.StudentStatusConsumer.as_asgi()), +] diff --git a/stude/student_status/urls.py b/stude/student_status/urls.py index 963e0cd..45c6b5c 100644 --- a/stude/student_status/urls.py +++ b/stude/student_status/urls.py @@ -3,5 +3,5 @@ from .views import StudentStatusAPIView, ActiveStudentStatusListAPIView urlpatterns = [ path('self/', StudentStatusAPIView.as_view()), - path('active_list/', ActiveStudentStatusListAPIView.as_view()), + path('list/', ActiveStudentStatusListAPIView.as_view()), ] From 05d9dbd29644895c9371720c821d8957fa5d7741 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sat, 1 Jul 2023 16:09:20 +0800 Subject: [PATCH 04/16] Polished student_status serializer --- stude/student_status/consumers.py | 9 +++++---- stude/student_status/serializers.py | 8 +++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/stude/student_status/consumers.py b/stude/student_status/consumers.py index 5c3affe..5d8a479 100644 --- a/stude/student_status/consumers.py +++ b/stude/student_status/consumers.py @@ -11,6 +11,7 @@ from djangochannelsrestframework.mixins import ( ListModelMixin, RetrieveModelMixin, ) +from djangochannelsrestframework.permissions import IsAuthenticated class StudentStatusConsumer( @@ -18,8 +19,8 @@ class StudentStatusConsumer( RetrieveModelMixin, GenericAsyncAPIConsumer, ): - - queryset = StudentStatus.objects.all() + permission_classes = [IsAuthenticated] + queryset = StudentStatus.objects.filter(active=True) serializer_class = StudentStatusSerializer async def websocket_connect(self, message): @@ -41,9 +42,9 @@ class StudentStatusConsumer( while True: try: data = await self.get_student_statuses() - print(f"Sending update: {data}") # existing debug statement + # print(f"Sending update: {data}") Debug await self.send(text_data=json.dumps(data)) - await asyncio.sleep(0.5) + await asyncio.sleep(5) except Exception as e: print(f"Exception in send_updates: {e}") break # Break the loop on error diff --git a/stude/student_status/serializers.py b/stude/student_status/serializers.py index 26768ad..4531a88 100644 --- a/stude/student_status/serializers.py +++ b/stude/student_status/serializers.py @@ -3,9 +3,11 @@ from .models import StudentStatus class StudentStatusSerializer(serializers.ModelSerializer): - year_level = serializers.CharField(source='user.year_level', read_only=True) + year_level = serializers.CharField( + source='user.year_level', read_only=True) course = serializers.CharField(source='user.course', read_only=True) semester = serializers.CharField(source='user.semester', read_only=True) + user = serializers.CharField(source='user.full_name', read_only=True) class Meta: model = StudentStatus @@ -17,7 +19,7 @@ class StudentStatusSerializer(serializers.ModelSerializer): student_status = StudentStatus.objects.create( user=user, defaults=validated_data) return student_status - + def update(self, instance, validated_data): active = validated_data.get('active', None) @@ -26,4 +28,4 @@ class StudentStatusSerializer(serializers.ModelSerializer): validated_data['y'] = None validated_data['subject'] = None - return super().update(instance, validated_data) \ No newline at end of file + return super().update(instance, validated_data) From e567b5d399b07e2efebf4f1d4292ca5470882a03 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 3 Jul 2023 21:22:06 +0800 Subject: [PATCH 05/16] Fixed user serializer still using django default user --- Pipfile | 1 + Pipfile.lock | 18 ++++++++++++++- stude/accounts/serializers.py | 41 ++++++++++++++++++++++++++--------- stude/config/settings.py | 2 +- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/Pipfile b/Pipfile index 6e5a135..c235739 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,7 @@ python-dotenv = "*" djoser = "*" pillow = "*" whitenoise = "*" +djangochannelsrestframework = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index af0eb49..b67ebf8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7a186c2a779282b0072d0634e264e5e70842a4c9676dfc4d1e18e564860e87c8" + "sha256": "231f11224ec1ef1ad1406ac5644ebcdfac020af9478f407d577553bb05c637df" }, "pipfile-spec": 6, "requires": { @@ -101,6 +101,14 @@ ], "version": "==1.15.1" }, + "channels": { + "hashes": [ + "sha256:0ce53507a7da7b148eaa454526e0e05f7da5e5d1c23440e4886cf146981d8420", + "sha256:2253334ac76f67cba68c2072273f7e0e67dbdac77eeb7e318f511d2f9a53c5e4" + ], + "markers": "python_version >= '3.7'", + "version": "==4.0.0" + }, "charset-normalizer": { "hashes": [ "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", @@ -230,6 +238,14 @@ ], "version": "==1.1.1" }, + "djangochannelsrestframework": { + "hashes": [ + "sha256:937260996b78fad66ddf4aa03dc61434b81b21a757897a899cd541d0f197c4ce", + "sha256:ca37fb96bb2f746129972a81dafed42d9785a37a2db36827dbf17848a0a9df96" + ], + "index": "pypi", + "version": "==1.1.0" + }, "djangorestframework": { "hashes": [ "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8", diff --git a/stude/accounts/serializers.py b/stude/accounts/serializers.py index 79b3298..89f8487 100644 --- a/stude/accounts/serializers.py +++ b/stude/accounts/serializers.py @@ -1,9 +1,13 @@ from djoser.serializers import UserCreateSerializer as BaseUserRegistrationSerializer from djoser.serializers import UserSerializer as BaseUserSerializer +from django.core import exceptions as django_exceptions +from rest_framework import exceptions as drf_exceptions from rest_framework import serializers from accounts.models import CustomUser from student_status.serializers import StudentStatusSerializer from student_status.models import StudentStatus +from rest_framework.settings import api_settings +from django.contrib.auth.password_validation import validate_password class CustomUserSerializer(BaseUserSerializer): @@ -15,20 +19,38 @@ class CustomUserSerializer(BaseUserSerializer): fields = ('username', 'email', 'password', 'student_id_number', 'year_level', 'semester', 'course', 'subjects', 'avatar', 'first_name', 'last_name', 'is_banned', 'user_status') +# The model from your custom user -class UserRegistrationSerializer(BaseUserRegistrationSerializer): - class Meta(BaseUserRegistrationSerializer.Meta): - fields = ('username', 'email', 'password', - 'student_id_number', 'year_level', 'semester', 'course', 'subjects', 'avatar', 'first_name', 'last_name') + +class UserRegistrationSerializer(serializers.ModelSerializer): + email = serializers.EmailField(required=True) + student_id_number = serializers.CharField(required=True) + password = serializers.CharField(write_only=True) + + class Meta: + model = CustomUser # Use your custom user model here + fields = ('username', 'email', 'password', 'student_id_number', + 'year_level', 'semester', 'course', 'subjects', 'avatar', + 'first_name', 'last_name') + + def validate(self, attrs): + user = self.Meta.model(**attrs) + password = attrs.get("password") + try: + validate_password(password, user) + except django_exceptions.ValidationError as e: + serializer_error = serializers.as_serializer_error(e) + raise serializers.ValidationError( + {"password": serializer_error[api_settings.NON_FIELD_ERRORS_KEY]} + ) + + return super().validate(attrs) def create(self, validated_data): - # Get the user's year_level and semester from the user model instance user = self.Meta.model(**validated_data) + user.set_password(validated_data['password']) + user.save() - # Create a new user using the base serializer's create() method - user = super().create(validated_data) - - # Create a student_status object for the user StudentStatus.objects.create( user=user, active=False, @@ -36,5 +58,4 @@ class UserRegistrationSerializer(BaseUserRegistrationSerializer): y=None, subject=None ) - return user diff --git a/stude/config/settings.py b/stude/config/settings.py index cd1dc15..3a18cd8 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -29,7 +29,7 @@ SECRET_KEY = str(os.getenv('SECRET_KEY')) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] +ALLOWED_HOSTS = ['*', '127.0.0.1', 'localhost', '10.0.10.32', '10.0.10.8'] # Email credentials EMAIL_HOST = '' From a7d5cdd56e7e665a9059c695207cf4d19f68198e Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 3 Jul 2023 23:19:03 +0800 Subject: [PATCH 06/16] Switch to JSON web token and set default host to 0.0.0.0 --- stude/accounts/urls.py | 2 +- stude/config/settings.py | 2 +- stude/manage.py | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/stude/accounts/urls.py b/stude/accounts/urls.py index 8df3791..22c121e 100644 --- a/stude/accounts/urls.py +++ b/stude/accounts/urls.py @@ -3,5 +3,5 @@ from django.urls import path, include urlpatterns = [ path('', include('djoser.urls')), - path('', include('djoser.urls.authtoken')), + path('', include('djoser.urls.jwt')), ] diff --git a/stude/config/settings.py b/stude/config/settings.py index 3a18cd8..a0e5621 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -109,7 +109,7 @@ TEMPLATES = [ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.TokenAuthentication', + 'rest_framework_simplejwt.authentication.JWTAuthentication', ) } diff --git a/stude/manage.py b/stude/manage.py index 8e7ac79..e0c0fbe 100644 --- a/stude/manage.py +++ b/stude/manage.py @@ -2,6 +2,7 @@ """Django's command-line utility for administrative tasks.""" import os import sys +from django.core.management.commands.runserver import Command as runserver def main(): @@ -19,4 +20,6 @@ def main(): if __name__ == '__main__': + # Override default port for `runserver` command + runserver.default_addr = '0.0.0.0' main() From 9196bfa60220e98d870125923dc9bcc254da9a66 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Tue, 4 Jul 2023 16:59:18 +0800 Subject: [PATCH 07/16] Remove authtoken from django admin since we are now using JWT --- stude/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stude/config/settings.py b/stude/config/settings.py index a0e5621..8ebcd62 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -60,8 +60,8 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'rest_framework_simplejwt', 'djoser', - 'rest_framework.authtoken', 'accounts', 'student_status', 'courses', From f6cbe1941e07dd8d44abce608742389c9f2568ff Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Tue, 4 Jul 2023 18:06:47 +0800 Subject: [PATCH 08/16] Fixed integrity issues with on_delete for customuser --- ...urse_alter_customuser_semester_and_more.py | 32 +++++++++++++++++++ stude/accounts/models.py | 6 ++-- stude/config/settings.py | 2 ++ .../0005_alter_studentstatus_subject.py | 20 ++++++++++++ stude/student_status/models.py | 2 +- 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 stude/accounts/migrations/0004_alter_customuser_course_alter_customuser_semester_and_more.py create mode 100644 stude/student_status/migrations/0005_alter_studentstatus_subject.py diff --git a/stude/accounts/migrations/0004_alter_customuser_course_alter_customuser_semester_and_more.py b/stude/accounts/migrations/0004_alter_customuser_course_alter_customuser_semester_and_more.py new file mode 100644 index 0000000..0527dd0 --- /dev/null +++ b/stude/accounts/migrations/0004_alter_customuser_course_alter_customuser_semester_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.2 on 2023-07-04 10:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('semesters', '0001_initial'), + ('courses', '0002_initial'), + ('year_levels', '0001_initial'), + ('accounts', '0003_customuser_subjects'), + ] + + operations = [ + migrations.AlterField( + model_name='customuser', + name='course', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='courses.course'), + ), + migrations.AlterField( + model_name='customuser', + name='semester', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='semesters.semester'), + ), + migrations.AlterField( + model_name='customuser', + name='year_level', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='year_levels.year_level'), + ), + ] diff --git a/stude/accounts/models.py b/stude/accounts/models.py index 24adb8e..80565bf 100644 --- a/stude/accounts/models.py +++ b/stude/accounts/models.py @@ -41,17 +41,17 @@ class CustomUser(AbstractUser): avatar = models.ImageField(upload_to=_get_upload_to, null=True) course = models.ForeignKey( Course, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, null=True ) year_level = models.ForeignKey( Year_Level, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, null=True ) semester = models.ForeignKey( Semester, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, null=True ) subjects = models.ManyToManyField( diff --git a/stude/config/settings.py b/stude/config/settings.py index 8ebcd62..a7ccf2b 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -182,3 +182,5 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +DOMAIN = 'stude://' diff --git a/stude/student_status/migrations/0005_alter_studentstatus_subject.py b/stude/student_status/migrations/0005_alter_studentstatus_subject.py new file mode 100644 index 0000000..845a322 --- /dev/null +++ b/stude/student_status/migrations/0005_alter_studentstatus_subject.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.2 on 2023-07-04 10:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('subjects', '0002_subjectstudent_subject_students'), + ('student_status', '0004_alter_studentstatus_study_group'), + ] + + operations = [ + migrations.AlterField( + model_name='studentstatus', + name='subject', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='subjects.subject'), + ), + ] diff --git a/stude/student_status/models.py b/stude/student_status/models.py index b2ca891..2787206 100644 --- a/stude/student_status/models.py +++ b/stude/student_status/models.py @@ -11,7 +11,7 @@ class StudentStatus(models.Model): x = models.FloatField(null=True) y = models.FloatField(null=True) subject = models.ForeignKey( - 'subjects.Subject', on_delete=models.CASCADE, null=True) + 'subjects.Subject', on_delete=models.SET_NULL, null=True) active = models.BooleanField(default=False) timestamp = models.DateField(auto_now_add=True) study_group = models.ManyToManyField( From 267e331be7a27aaa9aa664e4eb659e84bad017c1 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Tue, 4 Jul 2023 19:22:21 +0800 Subject: [PATCH 09/16] Polished email template and django admin for custom user --- stude/accounts/admin.py | 19 ++++++++++++++ stude/config/email.py | 5 ++++ stude/config/settings.py | 17 ++++++++++-- .../email_activation/email_activation.html | 26 +++++++++++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 stude/config/email.py create mode 100644 stude/templates/email_activation/email_activation.html diff --git a/stude/accounts/admin.py b/stude/accounts/admin.py index b071683..2ce5065 100644 --- a/stude/accounts/admin.py +++ b/stude/accounts/admin.py @@ -1,10 +1,29 @@ +from django import forms from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import CustomUser +from year_levels.models import Year_Level +from semesters.models import Semester +from courses.models import Course + + +class CustomUserForm(forms.ModelForm): + year_level = forms.ModelChoiceField( + queryset=Year_Level.objects.all(), required=False) + semester = forms.ModelChoiceField( + queryset=Semester.objects.all(), required=False) + course = forms.ModelChoiceField( + queryset=Course.objects.all(), required=False) + avatar = forms.ImageField(required=False) + + class Meta: + model = CustomUser + fields = '__all__' class CustomUserAdmin(UserAdmin): model = CustomUser + form = CustomUserForm fieldsets = UserAdmin.fieldsets + ( (None, {'fields': ('student_id_number', diff --git a/stude/config/email.py b/stude/config/email.py new file mode 100644 index 0000000..b7396aa --- /dev/null +++ b/stude/config/email.py @@ -0,0 +1,5 @@ +from djoser import email + + +class ActivationEmail(email.ActivationEmail): + template_name = 'email_activation/email_activation.html' diff --git a/stude/config/settings.py b/stude/config/settings.py index a7ccf2b..dcc63f8 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -36,6 +36,7 @@ EMAIL_HOST = '' EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' EMAIL_PORT = '' +EMAIL_USE_TLS = False if (DEBUG == True): EMAIL_HOST = str(os.getenv('DEV_EMAIL_HOST')) @@ -47,6 +48,7 @@ else: EMAIL_HOST_USER = str(os.getenv('PROD_EMAIL_HOST_USER')) EMAIL_HOST_PASSWORD = str(os.getenv('PROD_EMAIL_HOST_PASSWORD')) EMAIL_PORT = str(os.getenv('PROD_EMAIL_PORT')) + EMAIL_USE_TLS = str(os.getenv('PROD_EMAIL_TLS')) # Application definition @@ -94,7 +96,9 @@ ASGI_APPLICATION = "config.asgi.application" TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + BASE_DIR / 'templates', + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -131,6 +135,9 @@ AUTH_USER_MODEL = 'accounts.CustomUser' DJOSER = { 'SEND_ACTIVATION_EMAIL': True, 'SEND_CONFIRMATION_EMAIL': True, + 'EMAIL': { + 'activation': 'config.email.ActivationEmail' + }, 'ACTIVATION_URL': 'activation/{uid}/{token}', 'USER_AUTHENTICATION_RULES': ['djoser.authentication.TokenAuthenticationRule'], 'SERIALIZERS': { @@ -183,4 +190,10 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -DOMAIN = 'stude://' +DOMAIN = '' +if (DEBUG): + DOMAIN = 'exp' +else: + DOMAIN = 'stude' + +SITE_NAME = 'Stud-E' diff --git a/stude/templates/email_activation/email_activation.html b/stude/templates/email_activation/email_activation.html new file mode 100644 index 0000000..947d798 --- /dev/null +++ b/stude/templates/email_activation/email_activation.html @@ -0,0 +1,26 @@ +{% load i18n %} + +{% block subject %} +{% blocktrans %}Account activation on {{ site_name }}{% endblocktrans %} +{% endblock subject %} + +{% block text_body %} +{% blocktrans %}You're receiving this email because you need to finish activation process on {{ site_name }}.{% endblocktrans %} + +{% trans "Please open the following link to activate your account in-app:" %} +{{ domain }}://{{ url|safe }} + +{% trans "Thanks you for using StudE!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} +{% endblock text_body %} + +{% block html_body %} +

{% blocktrans %}You're receiving this email because you need to finish activation process on {{ site_name }}.{% endblocktrans %}

+ +

{% trans "Please go to the following page to activate your account in-app:" %}

+

{{ domain }}://--/{{ url|safe }}

+ +

{% blocktrans %}Many thnaks from the {{ site_name }} team{% endblocktrans %}

+ +{% endblock html_body %} \ No newline at end of file From 5840d3f95c576a34d03523cd4b08d14187dca169 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Tue, 4 Jul 2023 19:36:29 +0800 Subject: [PATCH 10/16] Make users not active by default, requiring email activation --- .../0005_alter_customuser_is_active.py | 18 ++++++++++++++++++ stude/accounts/models.py | 1 + stude/config/settings.py | 1 + 3 files changed, 20 insertions(+) create mode 100644 stude/accounts/migrations/0005_alter_customuser_is_active.py diff --git a/stude/accounts/migrations/0005_alter_customuser_is_active.py b/stude/accounts/migrations/0005_alter_customuser_is_active.py new file mode 100644 index 0000000..f6ebd78 --- /dev/null +++ b/stude/accounts/migrations/0005_alter_customuser_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-07-04 11:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_alter_customuser_course_alter_customuser_semester_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='customuser', + name='is_active', + field=models.BooleanField(default=False), + ), + ] diff --git a/stude/accounts/models.py b/stude/accounts/models.py index 80565bf..02b18fc 100644 --- a/stude/accounts/models.py +++ b/stude/accounts/models.py @@ -33,6 +33,7 @@ class CustomUser(AbstractUser): # Username inherited from base user class # Password inherited from base user class # is_admin inherited from base user class + is_active = models.BooleanField(default=False) is_student = models.BooleanField(default=True) is_studying = models.BooleanField(default=False) is_banned = models.BooleanField(default=False) diff --git a/stude/config/settings.py b/stude/config/settings.py index dcc63f8..c31d26f 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -196,4 +196,5 @@ if (DEBUG): else: DOMAIN = 'stude' + SITE_NAME = 'Stud-E' From 1e83f742174ed84987f858d0d2a5f951b5147522 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Tue, 4 Jul 2023 20:34:12 +0800 Subject: [PATCH 11/16] Improved CustomUserSerializer --- stude/accounts/serializers.py | 2 +- stude/config/settings.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/stude/accounts/serializers.py b/stude/accounts/serializers.py index 89f8487..ac5c23a 100644 --- a/stude/accounts/serializers.py +++ b/stude/accounts/serializers.py @@ -16,7 +16,7 @@ class CustomUserSerializer(BaseUserSerializer): class Meta(BaseUserSerializer.Meta): model = CustomUser - fields = ('username', 'email', 'password', + fields = ('username', 'email', 'student_id_number', 'year_level', 'semester', 'course', 'subjects', 'avatar', 'first_name', 'last_name', 'is_banned', 'user_status') # The model from your custom user diff --git a/stude/config/settings.py b/stude/config/settings.py index c31d26f..e519d94 100644 --- a/stude/config/settings.py +++ b/stude/config/settings.py @@ -142,6 +142,7 @@ DJOSER = { 'USER_AUTHENTICATION_RULES': ['djoser.authentication.TokenAuthenticationRule'], 'SERIALIZERS': { 'user': 'accounts.serializers.CustomUserSerializer', + 'current_user': 'accounts.serializers.CustomUserSerializer', 'user_create': 'accounts.serializers.UserRegistrationSerializer', }, } From ac268cb4c86089ad64aad7c21365f887a214fcfc Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Tue, 4 Jul 2023 20:49:28 +0800 Subject: [PATCH 12/16] Improved custom user serializer --- stude/accounts/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stude/accounts/serializers.py b/stude/accounts/serializers.py index ac5c23a..94518b1 100644 --- a/stude/accounts/serializers.py +++ b/stude/accounts/serializers.py @@ -13,11 +13,15 @@ from django.contrib.auth.password_validation import validate_password class CustomUserSerializer(BaseUserSerializer): user_status = StudentStatusSerializer( source='studentstatus', read_only=True) + course = serializers.StringRelatedField() + year_level = serializers.StringRelatedField() + semester = serializers.StringRelatedField() class Meta(BaseUserSerializer.Meta): model = CustomUser fields = ('username', 'email', 'student_id_number', 'year_level', 'semester', 'course', 'subjects', 'avatar', 'first_name', 'last_name', 'is_banned', 'user_status') + read_only_fields = ('is_banned', 'user_status') # The model from your custom user From 5f157d83b92432571a40d8fc3a227cd195e3c17c Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 6 Jul 2023 17:33:35 +0800 Subject: [PATCH 13/16] Fixed typo in email activation template --- stude/templates/email_activation/email_activation.html | 2 +- stude/year_levels/models.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stude/templates/email_activation/email_activation.html b/stude/templates/email_activation/email_activation.html index 947d798..03321f8 100644 --- a/stude/templates/email_activation/email_activation.html +++ b/stude/templates/email_activation/email_activation.html @@ -21,6 +21,6 @@

{% trans "Please go to the following page to activate your account in-app:" %}

{{ domain }}://--/{{ url|safe }}

-

{% blocktrans %}Many thnaks from the {{ site_name }} team{% endblocktrans %}

+

{% blocktrans %}Many thanks from the {{ site_name }} team{% endblocktrans %}

{% endblock html_body %} \ No newline at end of file diff --git a/stude/year_levels/models.py b/stude/year_levels/models.py index 02ddafb..c67c7d1 100644 --- a/stude/year_levels/models.py +++ b/stude/year_levels/models.py @@ -24,6 +24,6 @@ def populate_courses(sender, **kwargs): name='3rd Year', shortname='3rdYr') Year_Level.objects.get_or_create( name='4th Year', shortname='4thYr') - Year_Level.objects.get_or_create( - name='Irregular', shortname='Irreg') + # Year_Level.objects.get_or_create( + # name='Irregular', shortname='Irreg') # Add more predefined records as needed From a67d8ae106dc358d8ad5b5168c44f53cf8b7b476 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 6 Jul 2023 18:15:41 +0800 Subject: [PATCH 14/16] Fixed superuser being created as inactive user --- stude/accounts/models.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/stude/accounts/models.py b/stude/accounts/models.py index 02b18fc..4f33659 100644 --- a/stude/accounts/models.py +++ b/stude/accounts/models.py @@ -73,5 +73,10 @@ def create_superuser(sender, **kwargs): password = os.getenv('DJANGO_ADMIN_PASSWORD') if not User.objects.filter(username=username).exists(): - User.objects.create_superuser( - username, email, password) + # Create the superuser with is_active set to False + superuser = User.objects.create_superuser( + username=username, email=email, password=password) + + # Activate the superuser + superuser.is_active = True + superuser.save() From 288a1edd5e50add424c272277ef894c3c7d0946e Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 6 Jul 2023 21:47:28 +0800 Subject: [PATCH 15/16] Improved serializers --- stude/accounts/serializers.py | 12 +++++++++--- stude/study_groups/serializers.py | 6 ++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/stude/accounts/serializers.py b/stude/accounts/serializers.py index 94518b1..90f3b54 100644 --- a/stude/accounts/serializers.py +++ b/stude/accounts/serializers.py @@ -8,14 +8,20 @@ from student_status.serializers import StudentStatusSerializer from student_status.models import StudentStatus from rest_framework.settings import api_settings from django.contrib.auth.password_validation import validate_password +from courses.models import Course +from year_levels.models import Year_Level +from semesters.models import Semester class CustomUserSerializer(BaseUserSerializer): user_status = StudentStatusSerializer( source='studentstatus', read_only=True) - course = serializers.StringRelatedField() - year_level = serializers.StringRelatedField() - semester = serializers.StringRelatedField() + course = serializers.SlugRelatedField( + many=False, slug_field='name', queryset=Course.objects.all(), required=False, allow_null=True) + year_level = serializers.SlugRelatedField( + many=False, slug_field='name', queryset=Year_Level.objects.all(), required=False, allow_null=True) + semester = serializers.SlugRelatedField( + many=False, slug_field='name', queryset=Semester.objects.all(), required=False, allow_null=True) class Meta(BaseUserSerializer.Meta): model = CustomUser diff --git a/stude/study_groups/serializers.py b/stude/study_groups/serializers.py index bb3b0fe..5c1ec6e 100644 --- a/stude/study_groups/serializers.py +++ b/stude/study_groups/serializers.py @@ -1,12 +1,14 @@ from rest_framework import serializers from .models import StudyGroup, StudyGroupMembership from accounts.models import CustomUser +from subjects.models import Subject class StudyGroupSerializer(serializers.ModelSerializer): users = serializers.SlugRelatedField( - queryset=CustomUser.objects.all(), many=True, slug_field='name', allow_null=True) - subject = serializers.CharField(source='subject.Subject', read_only=True) + queryset=CustomUser.objects.all(), many=True, slug_field='name', required=False, allow_null=True) + subject = serializers.SlugRelatedField( + many=False, slug_field='name', queryset=Subject.objects.all(), required=True, allow_null=False) class Meta: model = StudyGroup From 07c57238a9bcd8d1ca005a107c833af3075dea89 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Fri, 7 Jul 2023 01:25:15 +0800 Subject: [PATCH 16/16] Fixed study group message serializer --- stude/studygroup_messages/serializers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stude/studygroup_messages/serializers.py b/stude/studygroup_messages/serializers.py index 5222e89..89b11da 100644 --- a/stude/studygroup_messages/serializers.py +++ b/stude/studygroup_messages/serializers.py @@ -1,13 +1,14 @@ from rest_framework import serializers from .models import Message from accounts.models import CustomUser +from study_groups.models import StudyGroup class MessageSerializer(serializers.ModelSerializer): user = serializers.SlugRelatedField( - queryset=CustomUser.objects.all(), slug_field='full_name', required=False) - study_group = serializers.CharField( - source='subject.Subject', read_only=True, required=False) + queryset=CustomUser.objects.all(), slug_field='full_name', required=True) + study_group = serializers.SlugRelatedField( + queryset=StudyGroup.objects.all(), slug_field='name', required=True) class Meta: model = Message