Merge pull request #1 from lemeow125/feature/websockets

Feature/websockets
This commit is contained in:
lemeow125 2023-07-07 01:26:03 +08:00 committed by GitHub
commit c09f502f28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 317 additions and 44 deletions

View file

@ -10,6 +10,7 @@ python-dotenv = "*"
djoser = "*"
pillow = "*"
whitenoise = "*"
djangochannelsrestframework = "*"
[dev-packages]

18
Pipfile.lock generated
View file

@ -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",

View file

@ -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',

View file

@ -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'),
),
]

View file

@ -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),
),
]

View file

@ -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)
@ -41,17 +42,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(
@ -72,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()

View file

@ -1,34 +1,66 @@
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
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.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
fields = ('username', 'email', 'password',
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
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 +68,4 @@ class UserRegistrationSerializer(BaseUserRegistrationSerializer):
y=None,
subject=None
)
return user

View file

@ -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')),
]

8
stude/api/routing.py Normal file
View file

@ -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))
]

View file

@ -1,16 +1,22 @@
"""
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, 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
# 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.)
'websocket': AuthMiddlewareStack(
URLRouter(
[re_path(r'ws/', URLRouter(api.routing.websocket_urlpatterns))]
)
),
})

5
stude/config/email.py Normal file
View file

@ -0,0 +1,5 @@
from djoser import email
class ActivationEmail(email.ActivationEmail):
template_name = 'email_activation/email_activation.html'

View file

@ -29,13 +29,14 @@ 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 = ''
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_PORT = ''
EMAIL_USE_TLS = False
if (DEBUG == True):
EMAIL_HOST = str(os.getenv('DEV_EMAIL_HOST'))
@ -47,11 +48,13 @@ 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
INSTALLED_APPS = [
'daphne',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -59,8 +62,8 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt',
'djoser',
'rest_framework.authtoken',
'accounts',
'student_status',
'courses',
@ -88,10 +91,14 @@ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
ROOT_URLCONF = 'config.urls'
ASGI_APPLICATION = "config.asgi.application"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [
BASE_DIR / 'templates',
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -106,7 +113,7 @@ TEMPLATES = [
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
@ -128,10 +135,14 @@ 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': {
'user': 'accounts.serializers.CustomUserSerializer',
'current_user': 'accounts.serializers.CustomUserSerializer',
'user_create': 'accounts.serializers.UserRegistrationSerializer',
},
}
@ -179,3 +190,12 @@ 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 = ''
if (DEBUG):
DOMAIN = 'exp'
else:
DOMAIN = 'stude'
SITE_NAME = 'Stud-E'

View file

@ -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()

View file

@ -0,0 +1,50 @@
# 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,
)
from djangochannelsrestframework.permissions import IsAuthenticated
class StudentStatusConsumer(
ListModelMixin,
RetrieveModelMixin,
GenericAsyncAPIConsumer,
):
permission_classes = [IsAuthenticated]
queryset = StudentStatus.objects.filter(active=True)
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}") Debug
await self.send(text_data=json.dumps(data))
await asyncio.sleep(5)
except Exception as e:
print(f"Exception in send_updates: {e}")
break # Break the loop on error

View file

@ -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'),
),
]

View file

@ -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(

View file

@ -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()),
]

View file

@ -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)
return super().update(instance, validated_data)

View file

@ -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()),
]

View file

@ -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

View file

@ -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

View file

@ -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 %}
<p>{% blocktrans %}You're receiving this email because you need to finish activation process on {{ site_name }}.{% endblocktrans %}</p>
<p>{% trans "Please go to the following page to activate your account in-app:" %}</p>
<p><a href="{{ domain }}://{{ url|safe }}">{{ domain }}://--/{{ url|safe }}</a></p>
<p>{% blocktrans %}Many thanks from the {{ site_name }} team{% endblocktrans %}</p>
{% endblock html_body %}

View file

@ -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