├── Auth ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_alter_emailverification_user.py │ ├── 0003_emailverification_user.py │ ├── 0001_initial.py │ └── 0002_emailverification_delete_emailotpverification.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── models.py ├── middleware.py └── views.py ├── Users ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0006_remove_user_is_active.py │ ├── 0005_user_is_verified.py │ ├── 0004_alter_user_profile_pic.py │ ├── 0001_initial.py │ ├── 0003_alter_user_options_remove_user_date_joined_and_more.py │ └── 0002_alter_user_options_user_date_joined_user_first_name_and_more.py ├── tests.py ├── admin.py ├── apps.py ├── urls.py ├── views.py ├── serializers.py └── models.py ├── Activities ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0001_initial.py │ └── 0002_option_remove_response_answer_alter_activity_room_and_more.py ├── tests.py ├── admin.py ├── apps.py ├── urls.py ├── routing.py ├── views.py ├── models.py ├── serializers.py └── consumer.py ├── ClassRoom ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_rename_topics_topic.py │ ├── 0007_participants_is_active.py │ ├── 0003_rename_description_topic_content_and_more.py │ ├── 0005_remove_classroom_lecturer_participants_is_lecturer.py │ ├── 0006_participantroomsettings_delete_studentroomsettings.py │ ├── 0001_initial.py │ └── 0004_participants_remove_classroom_students_and_more.py ├── admin.py ├── tests.py ├── routing.py ├── apps.py ├── signals.py ├── urls.py ├── models.py ├── serializers.py ├── views.py └── consumer.py ├── Documents ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── routing.py ├── models.py ├── serializer.py ├── views.py └── consumer.py ├── Messages ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── routing.py ├── models.py ├── serializer.py ├── views.py └── consumer.py ├── VideoCall ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── admin.py ├── tests.py ├── views.py ├── apps.py ├── routing.py └── consumer.py ├── Whiteboard ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── admin.py ├── apps.py ├── urls.py ├── routing.py ├── serializers.py ├── models.py ├── views.py └── consumer.py ├── learn_ease_backend ├── __init__.py ├── wsgi.py ├── routing.py ├── asgi.py ├── urls.py └── settings.py ├── .idea ├── vcs.xml ├── .gitignore ├── misc.xml ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── modules.xml ├── git_toolbox_prj.xml ├── learn_ease_backend.iml └── dataSources.xml ├── Dockerfile ├── requirements.txt ├── manage.py ├── templates └── email_verification_template.html ├── readme.md └── .gitignore /Auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Activities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ClassRoom/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Documents/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Messages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VideoCall/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Whiteboard/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Auth/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Activities/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ClassRoom/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Documents/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Messages/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VideoCall/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Whiteboard/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /learn_ease_backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Auth/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /Auth/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /VideoCall/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /Activities/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /ClassRoom/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /ClassRoom/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Documents/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /Documents/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Messages/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /Messages/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /VideoCall/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /VideoCall/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /VideoCall/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /Whiteboard/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Activities/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /Whiteboard/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /Auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'Auth' 7 | -------------------------------------------------------------------------------- /Users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'Users' 7 | -------------------------------------------------------------------------------- /Messages/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MessagesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'Messages' 7 | -------------------------------------------------------------------------------- /Documents/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DocumentsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'Documents' 7 | -------------------------------------------------------------------------------- /VideoCall/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class VideocallConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'VideoCall' 7 | -------------------------------------------------------------------------------- /Activities/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ActivitiesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'Activities' 7 | -------------------------------------------------------------------------------- /Users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Users.views import UpdateUserView 4 | 5 | urlpatterns = [ 6 | path('update/', UpdateUserView.as_view(), name='update-user'), 7 | ] 8 | -------------------------------------------------------------------------------- /Whiteboard/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WhiteboardConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'Whiteboard' 7 | -------------------------------------------------------------------------------- /Messages/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Messages.views import MessageView 4 | 5 | urlpatterns = [ 6 | path('/', MessageView.as_view(), name='Messages'), 7 | ] 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /VideoCall/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .consumer import VideoCallConsumer 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/video_call//', VideoCallConsumer.as_asgi()), 7 | ] 8 | -------------------------------------------------------------------------------- /Whiteboard/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Whiteboard.views import GetWhiteBoardData 4 | 5 | urlpatterns = [ 6 | path('/', GetWhiteBoardData.as_view(), name='Whiteboard') 7 | ] 8 | -------------------------------------------------------------------------------- /Documents/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Documents.views import DocumentCreateAPIView 4 | 5 | urlpatterns = [ 6 | path('/', DocumentCreateAPIView.as_view(), name='documents'), 7 | ] 8 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /Documents/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Documents.consumer import DocumentConsumer 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/documents//', DocumentConsumer.as_asgi()), 7 | ] 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Activities/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Activities.views import ListActivitiesView 4 | 5 | urlpatterns = [ 6 | path('/', ListActivitiesView.as_view(), name='create-list-activities'), 7 | ] 8 | -------------------------------------------------------------------------------- /Whiteboard/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Whiteboard.consumer import WhiteboardConsumer 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/whiteboard//', WhiteboardConsumer.as_asgi()), 7 | ] 8 | -------------------------------------------------------------------------------- /Messages/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Messages.consumer import MessageConsumer 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/messages//', MessageConsumer.as_asgi(), name='Messages'), 7 | ] 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /Activities/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from Activities.consumer import ActivityConsumer 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/activities//', ActivityConsumer.as_asgi(), name='Activities'), 7 | ] 8 | -------------------------------------------------------------------------------- /ClassRoom/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ClassRoom.consumer import ClassRoomConsumer 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/classroom//', ClassRoomConsumer.as_asgi(), name='ClassRoom'), 7 | ] 8 | -------------------------------------------------------------------------------- /ClassRoom/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ClassroomConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ClassRoom' 7 | 8 | def ready(self): 9 | import ClassRoom.signals 10 | -------------------------------------------------------------------------------- /Whiteboard/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | 3 | from Whiteboard.models import Whiteboard 4 | 5 | 6 | class WhiteboardSerializer(ModelSerializer): 7 | class Meta: 8 | model = Whiteboard 9 | fields = ('id', 'room_id', 'data', 'created_at', 'updated_at') -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | LABEL authors="shahsad" 4 | 5 | WORKDIR /usr/src/app 6 | 7 | COPY requirements.txt . 8 | RUN pip install -r requirements.txt 9 | 10 | COPY . . 11 | 12 | EXPOSE 8000 13 | 14 | ENTRYPOINT ["sh", "-c", "python manage.py migrate && daphne -b 0.0.0.0 learn_ease_backend.asgi:application"] -------------------------------------------------------------------------------- /Messages/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Message(models.Model): 5 | text = models.TextField() 6 | participant = models.ForeignKey('ClassRoom.Participants', on_delete=models.CASCADE) 7 | time = models.DateTimeField(auto_now_add=True) 8 | 9 | def __str__(self): 10 | return f'{self.text} - {self.participant.user.name}' 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 2 | Django==4.2.4 3 | pip==23.2.1 4 | setuptools==68.1.2 5 | sqlparse==0.4.4 6 | tzdata==2023.3 7 | djangorestframework==3.14.0 8 | django-environ==0.10.0 9 | psycopg2-binary==2.9.7 10 | djangorestframework-simplejwt==5.3.0 11 | Pillow==10.0.0 12 | django-cors-headers==4.2.0 13 | channels==4.0.0 14 | daphne==4.0.0 15 | channels-redis==4.1.0 -------------------------------------------------------------------------------- /Whiteboard/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Whiteboard(models.Model): 5 | room_id = models.IntegerField() 6 | data = models.TextField() 7 | created_at = models.DateTimeField(auto_now_add=True) 8 | updated_at = models.DateTimeField(auto_now=True) 9 | 10 | class Meta: 11 | db_table = 'whiteboards' 12 | ordering = ['-created_at'] 13 | -------------------------------------------------------------------------------- /ClassRoom/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save, post_delete 2 | from django.dispatch import receiver 3 | 4 | from ClassRoom.models import Participants, ParticipantRoomSettings 5 | 6 | 7 | @receiver(post_save, sender=Participants) 8 | def create_participant_room_settings(sender, instance, created, **kwargs): 9 | if created: 10 | ParticipantRoomSettings.objects.create(participant=instance) 11 | -------------------------------------------------------------------------------- /ClassRoom/migrations/0002_rename_topics_topic.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-24 12:55 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('ClassRoom', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameModel( 14 | old_name='Topics', 15 | new_name='Topic', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /Users/migrations/0006_remove_user_is_active.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-25 04:03 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('Users', '0005_user_is_verified'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='user', 15 | name='is_active', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /Documents/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Document(models.Model): 5 | title = models.CharField(max_length=255) 6 | docfile = models.FileField(upload_to='documents/%Y/%m/%d', blank=False, null=False) 7 | room = models.ForeignKey('ClassRoom.ClassRoom', on_delete=models.CASCADE) 8 | created_at = models.DateTimeField(auto_now_add=True) 9 | 10 | def __str__(self): 11 | return self.docfile.name 12 | -------------------------------------------------------------------------------- /ClassRoom/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ClassRoom.views import CreateClassRoom, GetTopics, GetClassRoom, GetHistory 4 | 5 | urlpatterns = [ 6 | path('create/', CreateClassRoom.as_view(), name='CreateClassRoom'), 7 | path('/', GetClassRoom.as_view(), name='GetTopics'), 8 | path('topics//', GetTopics.as_view(), name='GetTopics'), 9 | path('history/', GetHistory.as_view(), name='GetHistory'), 10 | ] 11 | -------------------------------------------------------------------------------- /Whiteboard/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import RetrieveAPIView 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from Whiteboard.models import Whiteboard 5 | from Whiteboard.serializers import WhiteboardSerializer 6 | 7 | 8 | class GetWhiteBoardData(RetrieveAPIView): 9 | serializer_class = WhiteboardSerializer 10 | permission_classes = [IsAuthenticated] 11 | lookup_field = 'room_id' 12 | lookup_url_kwarg = 'room_id' 13 | queryset = Whiteboard.objects.all() 14 | -------------------------------------------------------------------------------- /learn_ease_backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for learn_ease_backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'learn_ease_backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Users/migrations/0005_user_is_verified.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-25 03:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('Users', '0004_alter_user_profile_pic'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='is_verified', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /Documents/serializer.py: -------------------------------------------------------------------------------- 1 | from rest_framework.fields import SerializerMethodField 2 | from rest_framework.serializers import ModelSerializer 3 | 4 | from Documents.models import Document 5 | 6 | 7 | class DocumentSerializer(ModelSerializer): 8 | 9 | class Meta: 10 | model = Document 11 | fields = ('id', 'docfile', 'title') 12 | 13 | # def create(self, validated_data): 14 | # validated_data['room_id'] = self.context['view'].kwargs['room_id'] 15 | # return super().create(validated_data) 16 | -------------------------------------------------------------------------------- /Messages/serializer.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework.fields import SerializerMethodField 3 | 4 | from Messages.models import Message 5 | from Users.serializers import UserSerializer 6 | 7 | 8 | class MessageSerializer(serializers.ModelSerializer): 9 | sender = SerializerMethodField() 10 | 11 | class Meta: 12 | model = Message 13 | fields = ('text', 'sender', 'time') 14 | 15 | def get_sender(self, obj): 16 | return UserSerializer(obj.participant.user).data 17 | -------------------------------------------------------------------------------- /Activities/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListAPIView 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from Activities.models import Activity 5 | from Activities.serializers import ActivitySerializer 6 | 7 | 8 | class ListActivitiesView(ListAPIView): 9 | queryset = Activity.objects.all() 10 | serializer_class = ActivitySerializer 11 | permission_classes = [IsAuthenticated] 12 | 13 | def get_queryset(self): 14 | return Activity.objects.filter(room_id=self.kwargs['room_id']) 15 | -------------------------------------------------------------------------------- /Users/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import UpdateAPIView 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from Users.models import User 5 | from Users.serializers import UpdateUserSerializer 6 | 7 | 8 | class UpdateUserView(UpdateAPIView): 9 | """ 10 | View for updating a new user. 11 | """ 12 | queryset = User.objects.all() 13 | serializer_class = UpdateUserSerializer 14 | permission_classes = [IsAuthenticated] 15 | 16 | def get_object(self): 17 | return self.request.user 18 | -------------------------------------------------------------------------------- /ClassRoom/migrations/0007_participants_is_active.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-07-04 09:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('ClassRoom', '0006_participantroomsettings_delete_studentroomsettings'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='participants', 15 | name='is_active', 16 | field=models.BooleanField(default=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /Users/migrations/0004_alter_user_profile_pic.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-26 19:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('Users', '0003_alter_user_options_remove_user_date_joined_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='profile_pic', 16 | field=models.ImageField(default='profile_pics/default.jpg', upload_to='profile_pics/'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /Auth/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 3 | 4 | from Auth.views import LoginView, UserRegisterView, VerifyEmailView 5 | 6 | urlpatterns = [ 7 | path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 8 | path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 9 | path('login/', LoginView.as_view(), name='login-user'), 10 | path('register/', UserRegisterView.as_view(), name='register-user'), 11 | path('verify//', VerifyEmailView.as_view(), name='verify-email'), 12 | ] 13 | -------------------------------------------------------------------------------- /ClassRoom/migrations/0003_rename_description_topic_content_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-24 12:56 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('ClassRoom', '0002_rename_topics_topic'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='topic', 15 | old_name='description', 16 | new_name='content', 17 | ), 18 | migrations.RemoveField( 19 | model_name='topic', 20 | name='created_at', 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /Documents/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import CreateAPIView, ListAPIView 2 | 3 | from Documents.models import Document 4 | from Documents.serializer import DocumentSerializer 5 | 6 | 7 | class DocumentCreateAPIView(CreateAPIView, ListAPIView): 8 | serializer_class = DocumentSerializer 9 | queryset = Document.objects.all() 10 | 11 | def perform_create(self, serializer): 12 | classroom_id = self.kwargs.get('room_id') 13 | serializer.save(room_id=classroom_id) 14 | 15 | def get_queryset(self): 16 | classroom_id = self.kwargs.get('room_id') 17 | return Document.objects.filter(room_id=classroom_id) 18 | -------------------------------------------------------------------------------- /learn_ease_backend/routing.py: -------------------------------------------------------------------------------- 1 | from ClassRoom import routing as classroom_routing 2 | from Messages import routing as messages_routing 3 | from Documents import routing as documents_routing 4 | from Whiteboard import routing as whiteboard_routing 5 | from Activities import routing as grades_routing 6 | from VideoCall import routing as video_call_routing 7 | 8 | websocket_urlpatterns = [ 9 | *classroom_routing.websocket_urlpatterns, 10 | *messages_routing.websocket_urlpatterns, 11 | *documents_routing.websocket_urlpatterns, 12 | *whiteboard_routing.websocket_urlpatterns, 13 | *grades_routing.websocket_urlpatterns, 14 | *video_call_routing.websocket_urlpatterns, 15 | ] 16 | -------------------------------------------------------------------------------- /ClassRoom/migrations/0005_remove_classroom_lecturer_participants_is_lecturer.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-26 09:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('ClassRoom', '0004_participants_remove_classroom_students_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='classroom', 15 | name='lecturer', 16 | ), 17 | migrations.AddField( 18 | model_name='participants', 19 | name='is_lecturer', 20 | field=models.BooleanField(default=False), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /Auth/migrations/0004_alter_emailverification_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-25 03:44 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('Auth', '0003_emailverification_user'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='emailverification', 18 | name='user', 19 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'learn_ease_backend.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /Auth/migrations/0003_emailverification_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-25 03:41 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('Auth', '0002_emailverification_delete_emailotpverification'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='emailverification', 18 | name='user', 19 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /Auth/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.3 on 2023-07-30 17:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='EmailOtpVerification', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('email', models.EmailField(max_length=254)), 19 | ('otp', models.IntegerField()), 20 | ('created_at', models.DateTimeField(auto_now=True)), 21 | ('expired_at', models.DateTimeField()), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /Auth/migrations/0002_emailverification_delete_emailotpverification.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-24 14:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('Auth', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='EmailVerification', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('email', models.EmailField(max_length=254)), 18 | ('token', models.CharField(max_length=255)), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ], 21 | ), 22 | migrations.DeleteModel( 23 | name='EmailOtpVerification', 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /Messages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-26 19:18 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('ClassRoom', '0006_participantroomsettings_delete_studentroomsettings'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Message', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('text', models.TextField()), 21 | ('time', models.DateTimeField(auto_now_add=True)), 22 | ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ClassRoom.participants')), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /Whiteboard/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-07-03 06:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Whiteboard', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('room_id', models.IntegerField()), 19 | ('data', models.TextField()), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ], 23 | options={ 24 | 'db_table': 'whiteboards', 25 | 'ordering': ['-created_at'], 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /Documents/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-29 14:17 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('ClassRoom', '0006_participantroomsettings_delete_studentroomsettings'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Document', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=255)), 21 | ('docfile', models.FileField(upload_to='documents/%Y/%m/%d')), 22 | ('created_at', models.DateTimeField(auto_now_add=True)), 23 | ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ClassRoom.classroom')), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /Messages/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import IsAuthenticated 2 | from rest_framework.response import Response 3 | from rest_framework.status import HTTP_400_BAD_REQUEST 4 | from rest_framework.views import APIView 5 | 6 | from ClassRoom.models import ClassRoom, Participants 7 | from Messages.models import Message 8 | from Messages.serializer import MessageSerializer 9 | 10 | 11 | class MessageView(APIView): 12 | permission_classes = [IsAuthenticated] 13 | serializer_class = MessageSerializer 14 | 15 | def get(self, request, classroom_id): 16 | classroom = ClassRoom.objects.filter(id=classroom_id).first() 17 | if not classroom: 18 | return Response(status=HTTP_400_BAD_REQUEST) 19 | 20 | if not Participants.objects.filter(room=classroom, user=request.user).exists(): 21 | return Response(status=HTTP_400_BAD_REQUEST) 22 | 23 | messages = Message.objects.filter(participant__room_id=classroom.id) 24 | serializer = self.serializer_class(messages, many=True) 25 | return Response(serializer.data) 26 | -------------------------------------------------------------------------------- /Auth/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.mail import send_mail 3 | from django.db import models 4 | from django.template.loader import render_to_string 5 | 6 | from Users.models import User 7 | 8 | 9 | class EmailVerification(models.Model): 10 | email = models.EmailField() 11 | token = models.CharField(max_length=255) 12 | user = models.OneToOneField(User, on_delete=models.CASCADE) 13 | created_at = models.DateTimeField(auto_now_add=True) 14 | 15 | def __str__(self): 16 | return self.email 17 | 18 | def send(self): 19 | subject = 'Verify your email - LearnEase' 20 | verification_link = f"{settings.FRONTEND_URL}{settings.VERIFICATION_URL}{self.token}" 21 | message = render_to_string('email_verification_template.html', {'verification_link': verification_link}) 22 | return send_mail( 23 | subject=subject, 24 | message='', 25 | from_email=settings.EMAIL_HOST_USER, 26 | recipient_list=[self.email], 27 | html_message=message, 28 | ) 29 | -------------------------------------------------------------------------------- /Activities/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from ClassRoom.models import ClassRoom, Participants 4 | 5 | 6 | class Activity(models.Model): 7 | question = models.CharField(max_length=200) 8 | room = models.ForeignKey(ClassRoom, on_delete=models.CASCADE, related_name='activities') 9 | 10 | def __str__(self): 11 | return self.question 12 | 13 | 14 | class Option(models.Model): 15 | activity = models.ForeignKey(Activity, on_delete=models.CASCADE, related_name='options') 16 | option = models.CharField(max_length=200) 17 | correct = models.BooleanField(default=False) 18 | 19 | def __str__(self): 20 | return f"{self.option} - {self.activity}" 21 | 22 | 23 | class Response(models.Model): 24 | activity = models.ForeignKey(Activity, on_delete=models.CASCADE, related_name='responses') 25 | option = models.ForeignKey(Option, on_delete=models.CASCADE, related_name='responses') 26 | participant = models.ForeignKey(Participants, on_delete=models.CASCADE, related_name='responses') 27 | 28 | def __str__(self): 29 | return f"{self.option} - {self.participant}" 30 | -------------------------------------------------------------------------------- /Users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-23 06:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='User', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('password', models.CharField(max_length=128, verbose_name='password')), 19 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 20 | ('name', models.CharField(max_length=255)), 21 | ('email', models.EmailField(max_length=254, unique=True)), 22 | ('profile_pic', models.ImageField(blank=True, null=True, upload_to='profile_pics/')), 23 | ('is_active', models.BooleanField(default=True)), 24 | ('is_staff', models.BooleanField(default=False)), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /.idea/learn_ease_backend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | -------------------------------------------------------------------------------- /Users/migrations/0003_alter_user_options_remove_user_date_joined_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-23 11:48 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('Users', '0002_alter_user_options_user_date_joined_user_first_name_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='user', 15 | options={}, 16 | ), 17 | migrations.RemoveField( 18 | model_name='user', 19 | name='date_joined', 20 | ), 21 | migrations.RemoveField( 22 | model_name='user', 23 | name='first_name', 24 | ), 25 | migrations.RemoveField( 26 | model_name='user', 27 | name='groups', 28 | ), 29 | migrations.RemoveField( 30 | model_name='user', 31 | name='is_superuser', 32 | ), 33 | migrations.RemoveField( 34 | model_name='user', 35 | name='last_name', 36 | ), 37 | migrations.RemoveField( 38 | model_name='user', 39 | name='user_permissions', 40 | ), 41 | migrations.RemoveField( 42 | model_name='user', 43 | name='username', 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /ClassRoom/migrations/0006_participantroomsettings_delete_studentroomsettings.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-26 09:52 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 | ('ClassRoom', '0005_remove_classroom_lecturer_participants_is_lecturer'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ParticipantRoomSettings', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('audio_turned', models.BooleanField(default=False)), 19 | ('video_turned', models.BooleanField(default=False)), 20 | ('whiteboard_turned', models.BooleanField(default=False)), 21 | ('audio_permission', models.BooleanField(default=True)), 22 | ('video_permission', models.BooleanField(default=True)), 23 | ('whiteboard_permission', models.BooleanField(default=True)), 24 | ('participant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='ClassRoom.participants')), 25 | ], 26 | ), 27 | migrations.DeleteModel( 28 | name='StudentRoomSettings', 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /learn_ease_backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for learn_ease_backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from channels.layers import get_channel_layer 13 | from channels.routing import ProtocolTypeRouter, URLRouter 14 | from channels.security.websocket import OriginValidator 15 | from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler 16 | from django.core.asgi import get_asgi_application 17 | from environ import environ 18 | 19 | from Auth.middleware import JwtAuthMiddlewareStack 20 | from learn_ease_backend.routing import websocket_urlpatterns 21 | 22 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'learn_ease_backend.settings') 23 | 24 | env = environ.Env() 25 | environ.Env.read_env() 26 | 27 | application = ProtocolTypeRouter( 28 | { 29 | 'http': get_asgi_application(), 30 | 'websocket': OriginValidator( 31 | JwtAuthMiddlewareStack( 32 | URLRouter( 33 | routes=websocket_urlpatterns 34 | ) 35 | ), 36 | env('CORS_WHITELIST', default='').split(',') 37 | ) 38 | } 39 | ) 40 | 41 | application = ASGIStaticFilesHandler(application) 42 | 43 | channel_layer = get_channel_layer() 44 | -------------------------------------------------------------------------------- /Activities/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from rest_framework.fields import SerializerMethodField 3 | 4 | from Activities.models import Activity, Option 5 | from Activities.models import Response 6 | 7 | 8 | class OptionSerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = Option 11 | fields = ('id', 'option') 12 | 13 | 14 | class ResponseSerializer(serializers.ModelSerializer): 15 | activityId = serializers.PrimaryKeyRelatedField(source='activity', queryset=Activity.objects.all()) 16 | userId = serializers.SerializerMethodField() 17 | optionId = serializers.PrimaryKeyRelatedField(source='option', queryset=Option.objects.all()) 18 | isCorrect = serializers.BooleanField(source='option.correct', read_only=True) 19 | 20 | class Meta: 21 | model = Response 22 | fields = ('activityId', 'optionId', 'userId', 'isCorrect') 23 | 24 | def get_userId(self, obj: Response): 25 | return obj.participant.user.id 26 | 27 | 28 | class ActivitySerializer(serializers.ModelSerializer): 29 | options = SerializerMethodField() 30 | responses = SerializerMethodField() 31 | 32 | class Meta: 33 | model = Activity 34 | fields = ('id', 'question', 'options', 'responses') 35 | 36 | def get_options(self, obj): 37 | return OptionSerializer(obj.options.all(), many=True).data 38 | 39 | def get_responses(self, obj): 40 | return ResponseSerializer(obj.responses.all(), many=True).data 41 | -------------------------------------------------------------------------------- /Auth/middleware.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.db import database_sync_to_async 3 | from channels.middleware import BaseMiddleware 4 | from django.db import close_old_connections 5 | 6 | 7 | @database_sync_to_async 8 | def get_user_from_token(token): 9 | from django.contrib.auth.models import AnonymousUser 10 | from rest_framework_simplejwt.tokens import AccessToken 11 | from Users.models import User 12 | try: 13 | access_token = AccessToken(token) 14 | user = access_token.payload.get('user_id') 15 | return User.objects.get(id=user) 16 | except Exception: 17 | return AnonymousUser() 18 | 19 | 20 | class JwtAuthMiddleware(BaseMiddleware): 21 | def __init__(self, inner): 22 | super().__init__(inner) 23 | self.inner = inner 24 | 25 | async def __call__(self, scope, receive, send): 26 | from django.contrib.auth.models import AnonymousUser 27 | query_string: bytes = scope['query_string'] 28 | try: 29 | token = query_string.decode().split('=', 1)[1] 30 | except (KeyError, IndexError): 31 | token = None 32 | if token: 33 | scope['user'] = await get_user_from_token(token) 34 | else: 35 | scope['user'] = AnonymousUser() 36 | close_old_connections() 37 | return await super().__call__(scope, receive, send) 38 | 39 | 40 | def JwtAuthMiddlewareStack(inner): 41 | return JwtAuthMiddleware(AuthMiddlewareStack(inner)) 42 | -------------------------------------------------------------------------------- /Documents/consumer.py: -------------------------------------------------------------------------------- 1 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 2 | 3 | 4 | class DocumentConsumer(AsyncJsonWebsocketConsumer): 5 | def __init__(self, *args, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | self.channel_name = None 8 | self.room_group_name = None 9 | self.user = None 10 | self.class_room_id = None 11 | 12 | async def connect(self): 13 | await self.accept() 14 | if self.scope['user'].is_anonymous: 15 | await self.close(code=4001) 16 | return 17 | 18 | self.class_room_id = self.scope['url_route']['kwargs']['room_id'] 19 | self.room_group_name = f'document{self.class_room_id}' 20 | self.user = self.scope['user'] 21 | 22 | await self.channel_layer.group_add( 23 | self.room_group_name, 24 | self.channel_name, 25 | ) 26 | 27 | async def disconnect(self, close_code): 28 | await self.channel_layer.group_discard( 29 | self.room_group_name, 30 | self.channel_name, 31 | ) 32 | 33 | async def receive_json(self, content, **kwargs): 34 | document = content['document'] 35 | await self.channel_layer.group_send( 36 | self.room_group_name, 37 | { 38 | 'type': 'documents', 39 | 'document': document, 40 | } 41 | ) 42 | 43 | async def documents(self, event): 44 | document = event['document'] 45 | await self.send_json({ 46 | 'document': document 47 | }) 48 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | postgresql 6 | true 7 | org.postgresql.Driver 8 | jdbc:postgresql://localhost:5432/learnease 9 | 10 | 11 | 12 | $ProjectFileDir$ 13 | 14 | 15 | postgresql 16 | true 17 | DATABASE_NAME=learnease 18 | DATABASE_USERNAME=learnease 19 | DATABASE_PASSWORD=FsvgNc7Y8hQxNo0h6guzXV3RxEDoh6I7 20 | DATABASE_HOST=dpg-cjhjq0b37aks73br21a0-a.oregon-postgres.render.com 21 | DATABASE_PORT=5432 22 | org.postgresql.Driver 23 | jdbc:postgresql://dpg-cjhjq0b37aks73br21a0-a.oregon-postgres.render.com:5432/learnease 24 | 25 | 26 | 27 | $ProjectFileDir$ 28 | 29 | 30 | -------------------------------------------------------------------------------- /learn_ease_backend/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for learn_ease_backend project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.conf import settings 18 | from django.conf.urls.static import static 19 | from django.contrib import admin 20 | from django.urls import path, include 21 | 22 | urlpatterns = [ 23 | path('admin/', admin.site.urls), 24 | path('api/auth/', include(('Auth.urls', 'Auth'), namespace='Auth')), 25 | path('api/user/', include(('Users.urls', 'Users'), namespace='Users')), 26 | path('api/classroom/', include(('ClassRoom.urls', 'ClassRoom'), namespace='ClassRoom')), 27 | path('api/messages/', include(('Messages.urls', 'Messages'), namespace='Messages')), 28 | path('api/documents/', include(('Documents.urls', 'Documents'), namespace='Documents')), 29 | path('api/whiteboard/', include(('Whiteboard.urls', 'Whiteboard'), namespace='Whiteboard')), 30 | path('api/activities/', include(('Activities.urls', 'Activities'), namespace='Activities')), 31 | ] 32 | 33 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 34 | -------------------------------------------------------------------------------- /templates/email_verification_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Email Verification 8 | 39 | 40 | 41 |
42 |
43 |

Hello,

44 |

Thank you for registering with us. To verify your email address, please click the following link:

45 | Verify Email 46 |

If you didn't request this email, please discard it.

47 |

Note: This link is valid for one-time use only.

48 |
49 |
50 | 51 | -------------------------------------------------------------------------------- /ClassRoom/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-24 04:47 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ClassRoom', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=255)), 22 | ('created_at', models.DateTimeField(auto_now_add=True)), 23 | ('is_active', models.BooleanField(default=True)), 24 | ('lecturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='classrooms', to=settings.AUTH_USER_MODEL)), 25 | ('students', models.ManyToManyField(blank=True, related_name='classrooms_joined', to=settings.AUTH_USER_MODEL)), 26 | ], 27 | options={ 28 | 'ordering': ['-created_at'], 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='Topics', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('title', models.CharField(max_length=255)), 36 | ('description', models.TextField()), 37 | ('created_at', models.DateTimeField(auto_now_add=True)), 38 | ('class_room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='topics', to='ClassRoom.classroom')), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /ClassRoom/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class ClassRoom(models.Model): 5 | title = models.CharField(max_length=255) 6 | created_at = models.DateTimeField(auto_now_add=True) 7 | is_active = models.BooleanField(default=True) 8 | 9 | def __str__(self): 10 | return self.title + ' - ' + str(self.created_at) 11 | 12 | class Meta: 13 | ordering = ['-created_at'] 14 | 15 | 16 | class Topic(models.Model): 17 | title = models.CharField(max_length=255) 18 | content = models.TextField() 19 | class_room = models.ForeignKey(ClassRoom, on_delete=models.CASCADE, related_name='topics') 20 | 21 | def __str__(self): 22 | return self.title + ' - ' + str(self.class_room) 23 | 24 | 25 | class Participants(models.Model): 26 | room = models.ForeignKey(ClassRoom, on_delete=models.CASCADE, related_name='participants') 27 | user = models.ForeignKey('Users.User', on_delete=models.CASCADE, related_name='participated_rooms') 28 | is_active = models.BooleanField(default=True) 29 | is_lecturer = models.BooleanField(default=False) 30 | joined_at = models.DateTimeField(auto_now_add=True) 31 | 32 | def __str__(self): 33 | return self.room.title + ' - ' + self.user.name + ' - ' + str(self.joined_at) 34 | 35 | class Meta: 36 | ordering = ['-joined_at'] 37 | 38 | 39 | class ParticipantRoomSettings(models.Model): 40 | participant = models.OneToOneField(Participants, on_delete=models.CASCADE, related_name='settings') 41 | audio_turned = models.BooleanField(default=False) 42 | video_turned = models.BooleanField(default=False) 43 | whiteboard_turned = models.BooleanField(default=False) 44 | audio_permission = models.BooleanField(default=True) 45 | video_permission = models.BooleanField(default=True) 46 | whiteboard_permission = models.BooleanField(default=True) 47 | 48 | -------------------------------------------------------------------------------- /Activities/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-07-04 14:34 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('ClassRoom', '0007_participants_is_active'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Activity', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('question', models.CharField(max_length=200)), 21 | ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ClassRoom.classroom')), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Answer', 26 | fields=[ 27 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('answer', models.CharField(max_length=200)), 29 | ('correct', models.BooleanField(default=False)), 30 | ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Activities.activity')), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='Response', 35 | fields=[ 36 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Activities.activity')), 38 | ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='Activities.answer')), 39 | ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ClassRoom.participants')), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /Users/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.tokens import default_token_generator 2 | from rest_framework.fields import ImageField 3 | from rest_framework.serializers import ModelSerializer, BooleanField 4 | 5 | from Auth.models import EmailVerification 6 | from Users.models import User 7 | 8 | 9 | class UserSerializer(ModelSerializer): 10 | profilePicture = ImageField(source='profile_pic', required=False) 11 | isVerified = BooleanField(source='is_verified', required=False) 12 | 13 | class Meta: 14 | model = User 15 | fields = ['id', 'name', 'email', 'password', 'profilePicture', 'isVerified'] 16 | extra_kwargs = {'password': {'write_only': True, 'required': True}} 17 | 18 | def create(self, validated_data): 19 | user = User(**validated_data) 20 | user.set_password(validated_data.get('password')) 21 | user.save() 22 | return user 23 | 24 | 25 | class UpdateUserSerializer(ModelSerializer): 26 | profilePicture = ImageField(source='profile_pic', required=False) 27 | isVerified = BooleanField(source='is_verified', required=False) 28 | 29 | class Meta: 30 | model = User 31 | fields = ('id', 'name', 'email', 'profilePicture', 'isVerified') 32 | 33 | def to_representation(self, instance): 34 | representation = super().to_representation(instance) 35 | representation['profilePicture'] = instance.profile_pic.url 36 | return representation 37 | 38 | def update(self, instance, validated_data): 39 | instance = super().update(instance, validated_data) 40 | 41 | if 'email' in validated_data and instance.email != validated_data['email']: 42 | instance.is_verified = False 43 | instance.save() 44 | token = default_token_generator.make_token(instance) 45 | EmailVerification.objects.create(email=instance.email, token=token, user=instance).send() 46 | 47 | return instance 48 | -------------------------------------------------------------------------------- /Users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager 2 | from django.contrib.auth.models import AbstractUser 3 | from django.db import models 4 | 5 | 6 | class UserManager(BaseUserManager): 7 | 8 | def get_by_natural_key(self, email): 9 | """ 10 | Returns the user instance with the given email address. 11 | """ 12 | return self.get(email=email) 13 | 14 | def create_user(self, email, name, password=None): 15 | """ 16 | Creates and saves a User with the given email, name and password. 17 | """ 18 | if not email: 19 | raise ValueError('Users must have an email address') 20 | if not name: 21 | raise ValueError('Users must have a name') 22 | user = self.model( 23 | email=self.normalize_email(email), 24 | name=name, 25 | ) 26 | user.set_password(password) 27 | user.save(using=self._db) 28 | return user 29 | 30 | def create_superuser(self, email, name, password): 31 | """ 32 | Creates and saves a superuser with the given email, name and password. 33 | """ 34 | user = self.create_user( 35 | email=self.normalize_email(email), 36 | name=name, 37 | password=password, 38 | ) 39 | user.is_staff = True 40 | user.is_superuser = True 41 | user.save(using=self._db) 42 | return user 43 | 44 | 45 | class User(AbstractBaseUser): 46 | name = models.CharField(max_length=255) 47 | email = models.EmailField(unique=True) 48 | profile_pic = models.ImageField(upload_to='profile_pics/', default='profile_pics/default.jpg') 49 | is_staff = models.BooleanField(default=False) 50 | is_verified = models.BooleanField(default=False) 51 | 52 | USERNAME_FIELD = 'email' 53 | REQUIRED_FIELDS = ['name', 'password'] 54 | objects = UserManager() 55 | 56 | def __str__(self): 57 | return self.name 58 | -------------------------------------------------------------------------------- /Messages/consumer.py: -------------------------------------------------------------------------------- 1 | from asgiref.sync import sync_to_async 2 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 3 | 4 | 5 | @sync_to_async 6 | def add_message_to_db(message, user_id, room_id): 7 | from .models import Message 8 | from ClassRoom.models import Participants 9 | from .serializer import MessageSerializer 10 | try: 11 | participant = Participants.objects.filter(user_id=user_id, room_id=room_id).first() 12 | except Participants.DoesNotExist: 13 | raise KeyError('Participant does not exist') 14 | message = Message.objects.create(text=message, participant=participant) 15 | message.save() 16 | return MessageSerializer(message).data 17 | 18 | 19 | class MessageConsumer(AsyncJsonWebsocketConsumer): 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | self.channel_name = None 23 | self.room_group_name = None 24 | self.user = None 25 | self.class_room_id = None 26 | 27 | async def connect(self): 28 | await self.accept() 29 | if self.scope['user'].is_anonymous: 30 | await self.close(code=4001) 31 | return 32 | 33 | self.class_room_id = self.scope['url_route']['kwargs']['room_id'] 34 | self.room_group_name = f'message{self.class_room_id}' 35 | self.user = self.scope['user'] 36 | 37 | await self.channel_layer.group_add( 38 | self.room_group_name, 39 | self.channel_name, 40 | ) 41 | 42 | async def disconnect(self, close_code): 43 | await self.channel_layer.group_discard( 44 | self.room_group_name, 45 | self.channel_name, 46 | ) 47 | 48 | async def receive_json(self, content, **kwargs): 49 | message = content['message'] 50 | message_data = await add_message_to_db(message, self.user.id, self.class_room_id) 51 | await self.channel_layer.group_send( 52 | self.room_group_name, 53 | { 54 | 'type': 'chat_message', 55 | 'message': message_data, 56 | } 57 | ) 58 | 59 | async def chat_message(self, event): 60 | message = event['message'] 61 | await self.send_json({ 62 | 'message': message 63 | }) 64 | -------------------------------------------------------------------------------- /Activities/migrations/0002_option_remove_response_answer_alter_activity_room_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-07-04 15:21 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 | ('ClassRoom', '0007_participants_is_active'), 11 | ('Activities', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Option', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('option', models.CharField(max_length=200)), 20 | ('correct', models.BooleanField(default=False)), 21 | ], 22 | ), 23 | migrations.RemoveField( 24 | model_name='response', 25 | name='answer', 26 | ), 27 | migrations.AlterField( 28 | model_name='activity', 29 | name='room', 30 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='ClassRoom.classroom'), 31 | ), 32 | migrations.AlterField( 33 | model_name='response', 34 | name='activity', 35 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='Activities.activity'), 36 | ), 37 | migrations.AlterField( 38 | model_name='response', 39 | name='participant', 40 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='ClassRoom.participants'), 41 | ), 42 | migrations.DeleteModel( 43 | name='Answer', 44 | ), 45 | migrations.AddField( 46 | model_name='option', 47 | name='activity', 48 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='Activities.activity'), 49 | ), 50 | migrations.AddField( 51 | model_name='response', 52 | name='option', 53 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='Activities.option'), 54 | preserve_default=False, 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /Whiteboard/consumer.py: -------------------------------------------------------------------------------- 1 | from asgiref.sync import sync_to_async 2 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 3 | 4 | 5 | @sync_to_async 6 | def set_whiteboard_data(class_room_id, data): 7 | from Whiteboard.models import Whiteboard 8 | whiteboard, _ = Whiteboard.objects.update_or_create( 9 | room_id=class_room_id, 10 | defaults={ 11 | 'data': data 12 | } 13 | ) 14 | whiteboard.data = data 15 | whiteboard.save() 16 | 17 | 18 | class WhiteboardConsumer(AsyncJsonWebsocketConsumer): 19 | def __init__(self, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | self.channel_name = None 22 | self.room_group_name = None 23 | self.user = None 24 | self.class_room_id = None 25 | 26 | async def connect(self): 27 | await self.accept() 28 | if self.scope['user'].is_anonymous: 29 | await self.close(code=4001) 30 | return 31 | 32 | self.class_room_id = self.scope['url_route']['kwargs']['room_id'] 33 | self.room_group_name = f'whiteboard{self.class_room_id}' 34 | self.user = self.scope['user'] 35 | 36 | await self.channel_layer.group_add( 37 | self.room_group_name, 38 | self.channel_name, 39 | ) 40 | 41 | async def disconnect(self, close_code): 42 | await self.channel_layer.group_discard( 43 | self.room_group_name, 44 | self.channel_name, 45 | ) 46 | 47 | async def receive_json(self, content, **kwargs): 48 | if content['type'] == 'new_data': 49 | await set_whiteboard_data(self.class_room_id, content['data']) 50 | elif content['type'] == 'clear_data': 51 | await set_whiteboard_data(self.class_room_id, '') 52 | await self.channel_layer.group_send( 53 | self.room_group_name, 54 | { 55 | 'type': 'clear_data', 56 | } 57 | ) 58 | else: 59 | await self.channel_layer.group_send( 60 | self.room_group_name, 61 | content 62 | ) 63 | 64 | async def new_line(self, event): 65 | line = event['line'] 66 | await self.send_json({ 67 | 'type': 'line', 68 | 'line': line 69 | }) 70 | 71 | async def clear_data(self, _): 72 | await self.send_json({ 73 | 'type': 'clear_data' 74 | }) 75 | -------------------------------------------------------------------------------- /ClassRoom/migrations/0004_participants_remove_classroom_students_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-26 09:17 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('ClassRoom', '0003_rename_description_topic_content_and_more'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Participants', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('joined_at', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | options={ 23 | 'ordering': ['-joined_at'], 24 | }, 25 | ), 26 | migrations.RemoveField( 27 | model_name='classroom', 28 | name='students', 29 | ), 30 | migrations.CreateModel( 31 | name='StudentRoomSettings', 32 | fields=[ 33 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('audio_turned', models.BooleanField(default=False)), 35 | ('video_turned', models.BooleanField(default=False)), 36 | ('whiteboard_turned', models.BooleanField(default=False)), 37 | ('audio_permission', models.BooleanField(default=True)), 38 | ('video_permission', models.BooleanField(default=True)), 39 | ('whiteboard_permission', models.BooleanField(default=True)), 40 | ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='ClassRoom.participants')), 41 | ], 42 | ), 43 | migrations.AddField( 44 | model_name='participants', 45 | name='room', 46 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='ClassRoom.classroom'), 47 | ), 48 | migrations.AddField( 49 | model_name='participants', 50 | name='user', 51 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participated_rooms', to=settings.AUTH_USER_MODEL), 52 | ), 53 | migrations.AlterField( 54 | model_name='classroom', 55 | name='lecturer', 56 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='classrooms', to='ClassRoom.participants'), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /Users/migrations/0002_alter_user_options_user_date_joined_user_first_name_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.2 on 2023-06-23 11:47 2 | 3 | import django.contrib.auth.validators 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('auth', '0012_alter_user_first_name_max_length'), 12 | ('Users', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='user', 18 | options={'verbose_name': 'user', 'verbose_name_plural': 'users'}, 19 | ), 20 | migrations.AddField( 21 | model_name='user', 22 | name='date_joined', 23 | field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'), 24 | ), 25 | migrations.AddField( 26 | model_name='user', 27 | name='first_name', 28 | field=models.CharField(blank=True, max_length=150, verbose_name='first name'), 29 | ), 30 | migrations.AddField( 31 | model_name='user', 32 | name='groups', 33 | field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'), 34 | ), 35 | migrations.AddField( 36 | model_name='user', 37 | name='is_superuser', 38 | field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'), 39 | ), 40 | migrations.AddField( 41 | model_name='user', 42 | name='last_name', 43 | field=models.CharField(blank=True, max_length=150, verbose_name='last name'), 44 | ), 45 | migrations.AddField( 46 | model_name='user', 47 | name='user_permissions', 48 | field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), 49 | ), 50 | migrations.AddField( 51 | model_name='user', 52 | name='username', 53 | field=models.CharField(default='sdf', error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), 54 | preserve_default=False, 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /ClassRoom/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.fields import SerializerMethodField 2 | from rest_framework.serializers import ModelSerializer 3 | 4 | from ClassRoom.models import ClassRoom, Topic, Participants 5 | 6 | 7 | class ParticipantSettingsSerializer(ModelSerializer): 8 | audio = SerializerMethodField() 9 | video = SerializerMethodField() 10 | whiteboard = SerializerMethodField() 11 | 12 | class Meta: 13 | model = Participants 14 | fields = ['audio', 'video', 'whiteboard'] 15 | 16 | def get_audio(self, obj): 17 | return { 18 | 'permission': obj.audio_permission, 19 | 'enabled': obj.audio_turned, 20 | } 21 | 22 | def get_video(self, obj): 23 | return { 24 | 'permission': obj.video_permission, 25 | 'enabled': obj.video_turned, 26 | } 27 | 28 | def get_whiteboard(self, obj): 29 | return { 30 | 'permission': obj.whiteboard_permission, 31 | 'enabled': obj.whiteboard_turned, 32 | } 33 | 34 | 35 | class ParticipantSerializer(ModelSerializer): 36 | name = SerializerMethodField() 37 | profilePicture = SerializerMethodField() 38 | email = SerializerMethodField() 39 | settings = SerializerMethodField() 40 | id = SerializerMethodField() 41 | isActive = SerializerMethodField() 42 | 43 | class Meta: 44 | model = Participants 45 | fields = ['id', 'name', 'profilePicture', 'isActive', 'email', 'settings'] 46 | 47 | def get_id(self, obj): 48 | return obj.user.id 49 | 50 | def get_name(self, obj): 51 | return obj.user.name 52 | 53 | def get_profilePicture(self, obj): 54 | return obj.user.profile_pic.url if obj.user.profile_pic else None 55 | 56 | def get_email(self, obj): 57 | return obj.user.email 58 | 59 | def get_settings(self, obj: Participants): 60 | settings = obj.settings 61 | return ParticipantSettingsSerializer(settings).data 62 | 63 | def get_isActive(self, obj): 64 | return obj.is_active 65 | 66 | 67 | class ClassRoomSerializer(ModelSerializer): 68 | lecturer = SerializerMethodField() 69 | students = SerializerMethodField() 70 | 71 | class Meta: 72 | model = ClassRoom 73 | fields = ['id', 'title', 'lecturer', 'students', 'created_at'] 74 | 75 | def get_lecturer(self, obj): 76 | lecturer = obj.participants.filter(is_lecturer=True).first() 77 | return ParticipantSerializer(lecturer).data 78 | 79 | def get_students(self, obj): 80 | students = [] 81 | for student in obj.participants.filter(is_lecturer=False): 82 | students.append(ParticipantSerializer(student).data) 83 | return students 84 | 85 | 86 | class TopicSerializer(ModelSerializer): 87 | class Meta: 88 | model = Topic 89 | fields = ['id', 'title', 'content', 'class_room'] 90 | -------------------------------------------------------------------------------- /VideoCall/consumer.py: -------------------------------------------------------------------------------- 1 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 2 | 3 | 4 | class VideoCallConsumer(AsyncJsonWebsocketConsumer): 5 | def __init__(self, *args, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | self.channel_name = None 8 | self.chat_room_group_name = None 9 | self.class_room_id = None 10 | self.user = None 11 | 12 | async def connect(self): 13 | await self.accept() 14 | if self.scope['user'].is_anonymous: 15 | await self.close(code=4001) 16 | return 17 | 18 | self.class_room_id = self.scope['url_route']['kwargs'].get('room_id') 19 | self.chat_room_group_name = f'videocall{self.class_room_id}' 20 | self.user = self.scope['user'] 21 | 22 | await self.channel_layer.group_add(self.chat_room_group_name, self.channel_name, ) 23 | 24 | async def disconnect(self, close_code): 25 | await self.channel_layer.group_discard(self.chat_room_group_name, self.channel_name, ) 26 | 27 | async def receive_json(self, content, **kwargs): 28 | if content['type'] == 'join': 29 | await self.channel_layer.group_send(self.chat_room_group_name, 30 | {'type': 'join_student', 'from': self.user.id, }) 31 | 32 | elif content['type'] == 'offer': 33 | await self.channel_layer.group_send(self.chat_room_group_name, 34 | {'type': 'offer', 'from': self.user.id, 'to': content['userId'], 35 | 'offer': content['offer'], }) 36 | 37 | elif content['type'] == 'answer': 38 | await self.channel_layer.group_send(self.chat_room_group_name, 39 | {'type': 'answer', 'from': self.user.id, 'to': content['userId'], 40 | 'answer': content['answer'], }) 41 | 42 | elif content['type'] == 'ice-candidate': 43 | await self.channel_layer.group_send(self.chat_room_group_name, 44 | {'type': 'ice_candidate', 'from': self.user.id, 'to': content['userId'], 45 | 'candidate': content['candidate'], }) 46 | 47 | async def join_student(self, event): 48 | if event['from'] == self.user.id: return 49 | await self.send_json({'type': 'request-connection', 'userId': event['from'], }) 50 | 51 | async def offer(self, event): 52 | if event['to'] != self.user.id: 53 | return 54 | await self.send_json({'type': 'offer', 'userId': event['from'], 'offer': event['offer'], }) 55 | 56 | async def answer(self, event): 57 | if event['to'] != self.user.id: 58 | return 59 | await self.send_json({'type': 'answer', 'userId': event['from'], 'answer': event['answer'], }) 60 | 61 | async def ice_candidate(self, event): 62 | if event['to'] != self.user.id: 63 | return 64 | await self.send_json({'type': 'ice-candidate', 'userId': event['from'], 'candidate': event['candidate'], }) 65 | -------------------------------------------------------------------------------- /ClassRoom/views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from rest_framework.permissions import IsAuthenticated 4 | from rest_framework.request import Request 5 | from rest_framework.response import Response 6 | from rest_framework.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST 7 | from rest_framework.views import APIView 8 | 9 | from ClassRoom.models import ClassRoom, Participants 10 | from ClassRoom.serializers import ClassRoomSerializer, TopicSerializer 11 | 12 | 13 | class CreateClassRoom(APIView): 14 | permission_classes = [IsAuthenticated] 15 | 16 | def post(self, request: Request): 17 | data = request.data.copy() 18 | data['lecturer'] = request.user.pk 19 | topics_data = data.pop('topics', []) 20 | 21 | serializer = ClassRoomSerializer(data=data) 22 | if serializer.is_valid(): 23 | if not request.user.is_verified: 24 | return Response({'message': 'Please verify your email'}, status=HTTP_400_BAD_REQUEST) 25 | classroom = serializer.save() 26 | Participants.objects.create(room=classroom, user=request.user, is_lecturer=True) 27 | for topic_data in topics_data: 28 | topic_data['class_room'] = classroom.id 29 | topic_serializer = TopicSerializer(data=topic_data) 30 | if topic_serializer.is_valid(): 31 | topic_serializer.save() 32 | 33 | return Response(serializer.data, status=HTTP_201_CREATED) 34 | return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) 35 | 36 | 37 | class GetClassRoom(APIView): 38 | permission_classes = [IsAuthenticated] 39 | serializer_class = ClassRoomSerializer 40 | 41 | def get(self, request, classroom_id): 42 | classroom = ClassRoom.objects.filter(id=classroom_id).first() 43 | if not classroom: 44 | return Response(status=HTTP_400_BAD_REQUEST) 45 | 46 | # if not Participants.objects.filter(room=classroom, user=request.user).exists(): 47 | # return Response(status=HTTP_400_BAD_REQUEST) 48 | 49 | serializer = self.serializer_class(classroom) 50 | return Response(serializer.data) 51 | 52 | 53 | class GetTopics(APIView): 54 | permission_classes = [IsAuthenticated] 55 | serializer_class = TopicSerializer 56 | 57 | def get(self, request, classroom_id): 58 | classroom = ClassRoom.objects.filter(id=classroom_id).first() 59 | if not classroom: 60 | return Response(status=HTTP_400_BAD_REQUEST) 61 | 62 | if not Participants.objects.filter(room=classroom, user=request.user).exists(): 63 | return Response(status=HTTP_400_BAD_REQUEST) 64 | 65 | topics = classroom.topics.all() 66 | serializer = self.serializer_class(topics, many=True) 67 | return Response(serializer.data) 68 | 69 | 70 | class GetHistory(APIView): 71 | permission_classes = [IsAuthenticated] 72 | serializer_class = ClassRoomSerializer 73 | 74 | def get(self, request): 75 | last_week = datetime.today() - timedelta(days=7) 76 | participates = request.user.participated_rooms.select_related('room').filter(room__created_at__gte=last_week) 77 | classrooms = [participate.room for participate in participates] 78 | serializer = self.serializer_class(classrooms, many=True) 79 | return Response(serializer.data) 80 | -------------------------------------------------------------------------------- /Activities/consumer.py: -------------------------------------------------------------------------------- 1 | from asgiref.sync import sync_to_async 2 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 3 | 4 | 5 | @sync_to_async 6 | def create_activity(data, room_id): 7 | from Activities.models import Activity, Option 8 | from Activities.serializers import ActivitySerializer 9 | 10 | activity = Activity.objects.create( 11 | question=data['question'], 12 | room_id=room_id 13 | ) 14 | for option in data['options']: 15 | Option.objects.create( 16 | activity=activity, 17 | option=option, 18 | correct=option == data['correctAnswer'] 19 | ) 20 | return ActivitySerializer(activity).data 21 | 22 | 23 | @sync_to_async 24 | def register_response(data, user_id, room_id): 25 | from Activities.models import Response 26 | from Activities.serializers import ResponseSerializer 27 | from ClassRoom.models import Participants 28 | 29 | participant = Participants.objects.filter(user_id=user_id, room_id=room_id).first() 30 | if not participant: 31 | return None 32 | 33 | response = Response.objects.create( 34 | activity_id=data['activityId'], 35 | option_id=data['optionId'], 36 | participant=participant 37 | ) 38 | return ResponseSerializer(response).data 39 | 40 | 41 | class ActivityConsumer(AsyncJsonWebsocketConsumer): 42 | def __init__(self, *args, **kwargs): 43 | super().__init__(*args, **kwargs) 44 | self.channel_name = None 45 | self.chat_room_group_name = None 46 | self.user = None 47 | self.class_room_id = None 48 | 49 | async def connect(self): 50 | if self.scope['user'].is_anonymous: 51 | return await self.close(code=4004) 52 | 53 | self.class_room_id = self.scope['url_route']['kwargs']['room_id'] 54 | self.chat_room_group_name = f'activities_{self.class_room_id}' 55 | self.user = self.scope['user'] 56 | 57 | await self.channel_layer.group_add( 58 | self.chat_room_group_name, 59 | self.channel_name, 60 | ) 61 | await self.accept() 62 | 63 | async def disconnect(self, close_code): 64 | await self.channel_layer.group_discard( 65 | self.chat_room_group_name, 66 | self.channel_name, 67 | ) 68 | await self.close(code=404) 69 | 70 | async def receive_json(self, content, **kwargs): 71 | if content['type'] == 'new_activity': 72 | activity = await create_activity(content['activity'], self.class_room_id) 73 | await self.channel_layer.group_send( 74 | self.chat_room_group_name, 75 | { 76 | 'type': 'new_activity', 77 | 'activity': activity 78 | } 79 | ) 80 | elif content['type'] == 'new_response': 81 | response = await register_response(content['data'], self.user.id, self.class_room_id) 82 | if not response: 83 | return await self.close(code=4004) 84 | await self.channel_layer.group_send( 85 | self.chat_room_group_name, 86 | { 87 | 'type': 'new_response', 88 | 'response': response 89 | } 90 | ) 91 | pass 92 | 93 | async def new_activity(self, event): 94 | await self.send_json(event) 95 | 96 | async def new_response(self, event): 97 | await self.send_json(event) 98 | -------------------------------------------------------------------------------- /Auth/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate 2 | from django.contrib.auth.tokens import default_token_generator 3 | from rest_framework.generics import CreateAPIView 4 | from rest_framework.permissions import AllowAny, IsAuthenticated 5 | from rest_framework.response import Response 6 | from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_200_OK, HTTP_401_UNAUTHORIZED 7 | from rest_framework.views import APIView 8 | from rest_framework_simplejwt.tokens import RefreshToken 9 | 10 | from Auth.models import EmailVerification 11 | from Users.models import User 12 | from Users.serializers import UserSerializer 13 | 14 | 15 | class LoginView(APIView): 16 | """ 17 | View for logging in a user. 18 | """ 19 | permission_classes = [AllowAny] 20 | 21 | def post(self, request): 22 | email = request.data.get('email') 23 | password = request.data.get('password') 24 | if email is None or password is None: 25 | error = {} 26 | if email is None: 27 | error['email'] = ['This field is required.'] 28 | if password is None: 29 | error['password'] = ['This field is required.'] 30 | return Response(error, status=HTTP_400_BAD_REQUEST) 31 | user = authenticate(email=email, password=password) 32 | if not user: 33 | return Response({"detail": "No active account found with the given credentials"}, 34 | status=HTTP_401_UNAUTHORIZED) 35 | refresh = RefreshToken.for_user(user) 36 | data = { 37 | 'refresh': str(refresh), 38 | 'access': str(refresh.access_token), 39 | 'user': UserSerializer(user).data 40 | } 41 | return Response(data, status=HTTP_200_OK) 42 | 43 | 44 | class UserRegisterView(CreateAPIView): 45 | """ 46 | View for registering a new user. 47 | """ 48 | queryset = User.objects.all() 49 | serializer_class = UserSerializer 50 | permission_classes = [AllowAny] 51 | 52 | def post(self, request, *args, **kwargs): 53 | response = super().post(request, *args, **kwargs) 54 | if response.status_code == 201: 55 | serializer = self.get_serializer(data=request.data) 56 | serializer.is_valid() 57 | user = User.objects.get(email=serializer.data['email']) 58 | token = default_token_generator.make_token(user) 59 | EmailVerification.objects.create(email=user.email, token=token, user=user).send() 60 | 61 | refresh = RefreshToken.for_user(user) 62 | data = { 63 | 'refresh': str(refresh), 64 | 'access': str(refresh.access_token), 65 | 'user': UserSerializer(user).data 66 | } 67 | response.data = data 68 | return response 69 | 70 | 71 | class VerifyEmailView(APIView): 72 | """ 73 | View for verifying a user's email. 74 | """ 75 | permission_classes = [IsAuthenticated] 76 | 77 | def get(self, request, token): 78 | if request.user.is_verified: 79 | return Response({'detail': 'Email already verified.'}, status=HTTP_400_BAD_REQUEST) 80 | try: 81 | email_verification = EmailVerification.objects.get(token=token) 82 | user = request.user 83 | if user != email_verification.user: 84 | return Response({'detail': 'Invalid token.'}, status=HTTP_400_BAD_REQUEST) 85 | user.is_verified = True 86 | user.save() 87 | email_verification.delete() 88 | return Response({'detail': 'Email successfully verified.'}, status=HTTP_200_OK) 89 | except EmailVerification.DoesNotExist: 90 | return Response({'detail': 'Invalid token.'}, status=HTTP_400_BAD_REQUEST) 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://i.ibb.co/ngwjLL5/Unsastitled-2.png) 2 | 3 | 4 | # LearnEase - Backend 5 | 6 | ### 🚀 About LearnEase 7 | _LearnEase is an innovative virtual classroom application that takes video conferencing to the next level! With a focus on creating an engaging and interactive learning environment, LearnEase offers a plethora of exciting features designed to enrich the teaching and learning experience._ 8 | 9 | ## Features 10 | 11 | - Group Video: Engage in real-time video chats. 12 | - Live Whiteboard: Foster creativity and dynamic lessons. 13 | - Screenshare: Seamless sharing of multimedia content. 14 | - Live Chat: Encourage interactive discussions. 15 | - Document Sharing: Access study materials effortlessly. 16 | - Teaching Activities: Make learning enjoyable and impactful. 17 | - Grades: Track student progress efficiently. 18 | - JWT Auth: Secure and protected virtual environment. 19 | 20 | 21 | ## Installation and Setup 22 | 23 | #### 1 Prerequisites: 24 | - Make sure you have Python installed on your system. You can download and install Python from the official website: https://www.python.org/ 25 | - Install Postgres on your system. You can download and install Postgres from the official website: https://www.postgresql.org/ 26 | 27 | #### 2 Clone the Repository: 28 | 29 | - Open your terminal or command prompt. 30 | - Change the current working directory to where you want to store the LearnEase backend project. 31 | - Run the following command to clone the repository: 32 | ```bash 33 | git clone https://github.com/shahsad-kp/LearnEase-Server.git 34 | ``` 35 | 36 | #### 3 Create a Virtual Environment: 37 | - Change the current working directory to the project folder (e.g., LearnEase-Server). 38 | - Create a virtual environment using Python's venv or virtualenv: 39 | ```bash 40 | python -m venv venv 41 | ``` 42 | - Activate the virtual environment: 43 | - On Windows: 44 | ```bash 45 | venv\Scripts\activate 46 | ``` 47 | - On macOS and Linux: 48 | ```bash 49 | source venv/bin/activate 50 | ``` 51 | #### 4 Install Backend Dependencies: 52 | - With the virtual environment activated, run the following command to install the backend dependencies: 53 | ```bash 54 | pip install -r requirements.txt 55 | ``` 56 | 57 | #### 5 Environment Variables: 58 | d- Refer [environment variables](https://github.com/shahsad-kp/LearnEase-Server#environment-variables) for setting up the required environment variables for the LearnEase backend. 59 | 60 | 61 | #### 6 Apply Database Migrations: 62 | - Run the following command to apply the database migrations: 63 | ```bash 64 | python manage.py migrate 65 | ``` 66 | 67 | #### 7 Start the Daphne Server: 68 | - Run the following command to start the Daphne server with Channels: 69 | ```bash 70 | daphne learn_ease_backend.asgi:application 71 | ``` 72 | - The backend application will be accessible at http://localhost:8000 (or another available port if 8000 is already in use). 73 | 74 | That's it! You're all set to run the backend of LearnEase with Django, Daphne, Channels, and Postgres. Integrate this backend with the frontend to create a complete virtual classroom experience. Happy teaching and learning! 🚀📚 75 | 76 | ## Environment Variables 77 | 78 | To run this project, you will need to add the following environment variables to your .env file and store it settings folder: 79 | 80 | `SECRET_KEY` 81 | 82 | `DATABASE_NAME` 83 | 84 | `DATABASE_USERNAME` 85 | 86 | `DATABASE_PASSWORD` 87 | 88 | `DATABASE_HOST` 89 | 90 | `DATABASE_PORT` 91 | 92 | `CORS_WHITELIST` 93 | 94 | `ALLOWED_HOSTS` 95 | 96 | `DEBUG` 97 | 98 | `VERIFICATION_URL` 99 | 100 | `FRONTEND_URL` 101 | 102 | `EMAIL_HOST` 103 | 104 | `EMAIL_PORT` 105 | 106 | `EMAIL_HOST_USER` 107 | 108 | `EMAIL_HOST_PASSWORD` 109 | 110 | 111 | ## Feedback 112 | 113 | If you have any feedback, please reach me at shahsadkpklr@gmail.com or connect me at [LinkedIn](https://www.linkedin.com/in/shahsad-kp/) 114 | 115 | 116 | ## Support 117 | Show your support by 🌟 the project!! 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### venv template 2 | # Virtualenv 3 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 4 | .Python 5 | [Bb]in 6 | [Ii]nclude 7 | [Ll]ib 8 | [Ll]ib64 9 | [Ll]ocal 10 | [Ss]cripts 11 | pyvenv.cfg 12 | .venv 13 | pip-selfcheck.json 14 | 15 | ### VirtualEnv template 16 | # Virtualenv 17 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 18 | 19 | ### Django template 20 | *.log 21 | *.pot 22 | *.pyc 23 | __pycache__/ 24 | local_settings.py 25 | db.sqlite3 26 | db.sqlite3-journal 27 | media 28 | 29 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 30 | # in your Git repository. Update and uncomment the following line accordingly. 31 | # /staticfiles/ 32 | 33 | ### Python template 34 | # Byte-compiled / optimized / DLL files 35 | *.py[cod] 36 | *$py.class 37 | 38 | # C extensions 39 | *.so 40 | 41 | # Distribution / packaging 42 | build/ 43 | develop-eggs/ 44 | dist/ 45 | downloads/ 46 | eggs/ 47 | .eggs/ 48 | lib/ 49 | lib64/ 50 | parts/ 51 | sdist/ 52 | var/ 53 | wheels/ 54 | share/python-wheels/ 55 | *.egg-info/ 56 | .installed.cfg 57 | *.egg 58 | MANIFEST 59 | 60 | # PyInstaller 61 | # Usually these files are written by a python script from a template 62 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 63 | *.manifest 64 | *.spec 65 | 66 | # Installer logs 67 | pip-log.txt 68 | pip-delete-this-directory.txt 69 | 70 | # Unit test / coverage reports 71 | htmlcov/ 72 | .tox/ 73 | .nox/ 74 | .coverage 75 | .coverage.* 76 | .cache 77 | nosetests.xml 78 | coverage.xml 79 | *.cover 80 | *.py,cover 81 | .hypothesis/ 82 | .pytest_cache/ 83 | cover/ 84 | 85 | # Translations 86 | *.mo 87 | 88 | # Django stuff: 89 | 90 | # Flask stuff: 91 | instance/ 92 | .webassets-cache 93 | 94 | # Scrapy stuff: 95 | .scrapy 96 | 97 | # Sphinx documentation 98 | docs/_build/ 99 | 100 | # PyBuilder 101 | .pybuilder/ 102 | target/ 103 | 104 | # Jupyter Notebook 105 | .ipynb_checkpoints 106 | 107 | # IPython 108 | profile_default/ 109 | ipython_config.py 110 | 111 | # pyenv 112 | # For a library or package, you might want to ignore these files since the code is 113 | # intended to run in multiple environments; otherwise, check them in: 114 | # .python-version 115 | 116 | # pipenv 117 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 118 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 119 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 120 | # install all needed dependencies. 121 | #Pipfile.lock 122 | 123 | # poetry 124 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 125 | # This is especially recommended for binary packages to ensure reproducibility, and is more 126 | # commonly ignored for libraries. 127 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 128 | #poetry.lock 129 | 130 | # pdm 131 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 132 | #pdm.lock 133 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 134 | # in version control. 135 | # https://pdm.fming.dev/#use-with-ide 136 | .pdm.toml 137 | 138 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 139 | __pypackages__/ 140 | 141 | # Celery stuff 142 | celerybeat-schedule 143 | celerybeat.pid 144 | 145 | # SageMath parsed files 146 | *.sage.py 147 | 148 | # Environments 149 | learn_ease_backend/.env 150 | env/ 151 | venv/ 152 | ENV/ 153 | env.bak/ 154 | venv.bak/ 155 | 156 | # Spyder project settings 157 | .spyderproject 158 | .spyproject 159 | 160 | # Rope project settings 161 | .ropeproject 162 | 163 | # mkdocs documentation 164 | /site 165 | 166 | # mypy 167 | .mypy_cache/ 168 | .dmypy.json 169 | dmypy.json 170 | 171 | # Pyre type checker 172 | .pyre/ 173 | 174 | # pytype static type analyzer 175 | .pytype/ 176 | 177 | # Cython debug symbols 178 | cython_debug/ 179 | 180 | # PyCharm 181 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 182 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 183 | # and can be added to the global gitignore or merged into this file. For a more nuclear 184 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 185 | #.idea/ 186 | 187 | -------------------------------------------------------------------------------- /learn_ease_backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django's settings for learn_ease_backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | from datetime import timedelta 13 | from pathlib import Path 14 | 15 | from environ import environ 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 22 | 23 | # Initialise environment variables 24 | env = environ.Env() 25 | environ.Env.read_env() 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = env('SECRET_KEY') 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = env('DEBUG') == 'True' 32 | 33 | ALLOWED_HOSTS = env('ALLOWED_HOSTS', default='').split(',') 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | 'daphne', 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | 'channels', 46 | 'rest_framework', 47 | 'rest_framework_simplejwt', 48 | 'corsheaders', 49 | 'Auth.apps.AuthConfig', 50 | 'Users.apps.UsersConfig', 51 | 'ClassRoom.apps.ClassroomConfig', 52 | 'Messages.apps.MessagesConfig', 53 | 'Documents.apps.DocumentsConfig', 54 | 'Whiteboard.apps.WhiteboardConfig', 55 | 'Activities.apps.ActivitiesConfig', 56 | ] 57 | 58 | MIDDLEWARE = [ 59 | 'django.middleware.security.SecurityMiddleware', 60 | 'django.contrib.sessions.middleware.SessionMiddleware', 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.middleware.csrf.CsrfViewMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | 'django.contrib.messages.middleware.MessageMiddleware', 65 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 66 | 'corsheaders.middleware.CorsMiddleware', 67 | ] 68 | 69 | ROOT_URLCONF = 'learn_ease_backend.urls' 70 | 71 | TEMPLATES = [ 72 | { 73 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 74 | 'DIRS': [BASE_DIR / 'templates'], 75 | 'APP_DIRS': True, 76 | 'OPTIONS': { 77 | 'context_processors': [ 78 | 'django.template.context_processors.debug', 79 | 'django.template.context_processors.request', 80 | 'django.contrib.auth.context_processors.auth', 81 | 'django.contrib.messages.context_processors.messages', 82 | ], 83 | }, 84 | }, 85 | ] 86 | 87 | # WSGI_APPLICATION = 'learn_ease_backend.wsgi.application' 88 | ASGI_APPLICATION = 'learn_ease_backend.asgi.application' 89 | 90 | # Database 91 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 92 | 93 | DATABASES = { 94 | 'default': { 95 | 'ENGINE': 'django.db.backends.postgresql', 96 | 'NAME': env('DATABASE_NAME'), 97 | 'USER': env('DATABASE_USERNAME'), 98 | 'PASSWORD': env('DATABASE_PASSWORD'), 99 | 'HOST': env('DATABASE_HOST'), 100 | 'POST': env('DATABASE_PORT') 101 | } 102 | } 103 | 104 | REST_FRAMEWORK = { 105 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 106 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 107 | ) 108 | } 109 | 110 | # Password validation 111 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 112 | 113 | AUTH_PASSWORD_VALIDATORS = [ 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 119 | }, 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 122 | }, 123 | { 124 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 125 | }, 126 | ] 127 | 128 | CORS_ORIGIN_ALLOW_ALL = False 129 | CORS_ALLOWED_ORIGINS = env('CORS_WHITELIST', default='').split(',') 130 | 131 | SIMPLE_JWT = { 132 | "ACCESS_TOKEN_LIFETIME": timedelta(minutes=1), 133 | "REFRESH_TOKEN_LIFETIME": timedelta(days=1), 134 | } 135 | 136 | # Email settings 137 | EMAIL_USE_TLS = True 138 | EMAIL_HOST = env('EMAIL_HOST') 139 | EMAIL_HOST_USER = env('EMAIL_HOST_USER') 140 | EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') 141 | EMAIL_PORT = env('EMAIL_PORT') 142 | 143 | # Internationalization 144 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 145 | 146 | LANGUAGE_CODE = 'en-us' 147 | 148 | TIME_ZONE = 'UTC' 149 | 150 | USE_I18N = True 151 | 152 | USE_TZ = True 153 | 154 | # Static files (CSS, JavaScript, Images) 155 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 156 | 157 | STATIC_URL = 'static/' 158 | 159 | # Default primary key field type 160 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 161 | 162 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 163 | 164 | AUTH_USER_MODEL = 'Users.User' 165 | 166 | FRONTEND_URL = env('FRONTEND_URL') 167 | VERIFICATION_URL = env('VERIFICATION_URL') 168 | 169 | # Media files 170 | MEDIA_URL = env('MEDIA_URL') 171 | MEDIA_ROOT = BASE_DIR / 'media' 172 | 173 | CHANNEL_LAYERS = { 174 | "default": { 175 | "BACKEND": "channels.layers.InMemoryChannelLayer" 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 103 | -------------------------------------------------------------------------------- /ClassRoom/consumer.py: -------------------------------------------------------------------------------- 1 | from asgiref.sync import sync_to_async 2 | from channels.generic.websocket import AsyncJsonWebsocketConsumer 3 | 4 | 5 | @sync_to_async 6 | def add_participant(class_room_id: int, user_id: int): 7 | from ClassRoom.models import ClassRoom, Participants 8 | from ClassRoom.serializers import ParticipantSerializer 9 | try: 10 | class_room = ClassRoom.objects.get(id=class_room_id) 11 | except ClassRoom.DoesNotExist: 12 | raise KeyError('ClassRoom does not exist') 13 | participant = class_room.participants.filter(user_id=user_id).first() 14 | if participant and participant.is_active: 15 | return ParticipantSerializer(participant).data 16 | elif participant: 17 | participant.is_active = True 18 | participant.save() 19 | return ParticipantSerializer(participant).data 20 | participant = Participants.objects.create(user_id=user_id, room_id=class_room_id) 21 | participant.save() 22 | return ParticipantSerializer(participant).data 23 | 24 | 25 | @sync_to_async 26 | def remove_participant(class_room_id: int, user_id: int): 27 | from ClassRoom.models import ClassRoom 28 | try: 29 | class_room = ClassRoom.objects.get(id=class_room_id) 30 | except ClassRoom.DoesNotExist: 31 | raise KeyError('ClassRoom does not exist') 32 | participant = class_room.participants.filter(user_id=user_id).first() 33 | if not participant: 34 | return 35 | participant.is_active = False 36 | participant.save() 37 | 38 | 39 | @sync_to_async 40 | def change_settings(user_id: int, class_room_id: int, audio: bool, video: bool): 41 | from ClassRoom.models import Participants 42 | try: 43 | participant = Participants.objects.get(user_id=user_id, room_id=class_room_id) 44 | except Participants.DoesNotExist: 45 | raise KeyError('Participant does not exist') 46 | participant.settings.audio_turned = audio 47 | participant.settings.video_turned = video 48 | participant.settings.save() 49 | from ClassRoom.serializers import ParticipantSettingsSerializer 50 | return ParticipantSettingsSerializer(participant.settings).data 51 | 52 | 53 | @sync_to_async 54 | def change_permissions(user_id: int, target_user_id, class_room_id: int, audio: bool, video: bool): 55 | from ClassRoom.models import Participants 56 | try: 57 | Participants.objects.get(user_id=user_id, room_id=class_room_id, is_lecturer=True) 58 | except Participants.DoesNotExist: 59 | raise PermissionError('You are not a lecturer') 60 | try: 61 | participant = Participants.objects.get(user_id=target_user_id, room_id=class_room_id) 62 | except Participants.DoesNotExist: 63 | raise KeyError('Participant does not exist') 64 | 65 | if audio is not None: 66 | participant.settings.audio_permission = audio 67 | if video is not None: 68 | participant.settings.video_permission = video 69 | participant.settings.save() 70 | from ClassRoom.serializers import ParticipantSettingsSerializer 71 | return ParticipantSettingsSerializer(participant.settings).data 72 | 73 | 74 | class ClassRoomConsumer(AsyncJsonWebsocketConsumer): 75 | def __init__(self, *args, **kwargs): 76 | super().__init__(*args, **kwargs) 77 | self.channel_name = None 78 | self.chat_room_group_name = None 79 | self.user = None 80 | self.class_room_id = None 81 | 82 | async def connect(self): 83 | await self.accept() 84 | if self.scope['user'].is_anonymous: 85 | await self.close(code=4001) 86 | return 87 | self.class_room_id = self.scope['url_route']['kwargs']['room_id'] 88 | self.chat_room_group_name = f'classroom_{self.class_room_id}' 89 | self.user = self.scope['user'] 90 | 91 | try: 92 | participant = await add_participant(self.class_room_id, self.user.id) 93 | except KeyError: 94 | await self.close(code=4004) 95 | return 96 | await self.channel_layer.group_send(self.chat_room_group_name, 97 | {'type': 'join_student', 'student': participant, }) 98 | await self.channel_layer.group_add(self.chat_room_group_name, self.channel_name, ) 99 | 100 | async def disconnect(self, event): 101 | if not self.user: 102 | return 103 | 104 | await self.channel_layer.group_send(self.chat_room_group_name, 105 | {'type': 'leave_student', 'student_id': self.user.id, }) 106 | await self.channel_layer.group_discard(self.chat_room_group_name, self.channel_name, ) 107 | try: 108 | await remove_participant(self.class_room_id, self.user.id) 109 | except KeyError: 110 | pass 111 | await self.close() 112 | 113 | async def receive_json(self, content, **kwargs): 114 | if content['type'] == 'change_settings': 115 | try: 116 | audio = content['audio'] 117 | video = content['video'] 118 | except KeyError: 119 | return 120 | new_settings = await change_settings(self.user.id, self.class_room_id, audio, video) 121 | await self.channel_layer.group_send(self.chat_room_group_name, 122 | {'type': 'change_settings', 123 | 'user_id': self.user.id, 124 | 'settings': new_settings, }) 125 | elif content['type'] == 'change_permission': 126 | target_user_id = content['user_id'] 127 | permissions = content['permission'] 128 | audio = permissions.get('audio') 129 | video = permissions.get('video') 130 | try: 131 | new_settings = await change_permissions(self.user.id, target_user_id, self.class_room_id, audio, video) 132 | except [KeyError, PermissionError]: 133 | return 134 | await self.channel_layer.group_send(self.chat_room_group_name, 135 | {'type': 'change_settings', 'user_id': target_user_id, 136 | 'settings': new_settings, }) 137 | 138 | async def join_student(self, event): 139 | await self.send_json({'type': 'join_student', 'student': event['student']}) 140 | 141 | async def leave_student(self, event): 142 | await self.send_json({'type': 'leave_student', 'student_id': event['student_id']}) 143 | 144 | async def change_settings(self, event): 145 | await self.send_json({'type': 'change_settings', 'user_id': event['user_id'], 'settings': event['settings'], }) 146 | --------------------------------------------------------------------------------