diff --git a/Lone_Developers/Lone_Developers.pptx b/Lone_Developers/Lone_Developers.pptx new file mode 100644 index 0000000..f3fe493 Binary files /dev/null and b/Lone_Developers/Lone_Developers.pptx differ diff --git a/Lone_Developers/Lone_Developers.txt b/Lone_Developers/Lone_Developers.txt new file mode 100644 index 0000000..a0e41b2 --- /dev/null +++ b/Lone_Developers/Lone_Developers.txt @@ -0,0 +1,22 @@ +Description of your project +--------------------------- + +As schools around the world respond to the “new normal”, the need for remote learning tools has never been more urgent. We present a solution that can help schools, educators, students, and their families to make the transition to distance learning easier. +We Developed pseudo distance learning applications or online educational tools for remote learning that allow users to interact and a “classroom-like” environment. + +Some Features: + +1) Users can register as students, Teachers, or Parents. +2) Virtual study group — Students can meet up on a common forum and prepare for their exams along with other students that are studying for the same subject matter. +3) Provide users with study material, tools, discussion helps guides, etc. +4)Parents can check the notice board for any announcement (like snow/rainy day holidays, reports cards, fee due dates, attendance as well as upcoming school events) rather than communicating via email. +5)Parents/Teachers can upload photos, videos and other files that they may feel are important for children’s education. + +Contents of project directory +----------------------------- + +The project directory contains modular and structured code for the hosted web application. It is written in Python language using Django Framework. + +To Host locally +--------------- +Please follow Readme inside the project directory to see the installation process. diff --git a/Lone_Developers/Lone_Developers/.gitignore b/Lone_Developers/Lone_Developers/.gitignore new file mode 100644 index 0000000..ee64ffa --- /dev/null +++ b/Lone_Developers/Lone_Developers/.gitignore @@ -0,0 +1,2 @@ +final/ +venv/ diff --git a/Lone_Developers/Lone_Developers/Procfile b/Lone_Developers/Lone_Developers/Procfile new file mode 100644 index 0000000..e06cc3e --- /dev/null +++ b/Lone_Developers/Lone_Developers/Procfile @@ -0,0 +1,3 @@ +release: python manage.py migrate +web: daphne hack72.asgi:application --port $PORT --bind 0.0.0.0 -v2 +worker: python manage.py runworker --settings=hack72.settings -v2 diff --git a/Lone_Developers/Lone_Developers/README.md b/Lone_Developers/Lone_Developers/README.md new file mode 100644 index 0000000..83a1358 --- /dev/null +++ b/Lone_Developers/Lone_Developers/README.md @@ -0,0 +1,84 @@ +# Code-Innovation-Series-IITG + +An Online education platform +Live at https://learn-live.herokuapp.com/ + +## Test User Credentials + +Login -> http://learn-live.herokuapp.com/accounts/login/ + +### Student Login +Username : student01 +Password : common101 + +### Teacher Login +Username : teacher01 +Password : common101 + +### Parent Login +Username : parent01 +Password : common101 + +### Django SuperUser +login -> http://learn-live.herokuapp.com/admin +Username : kunal +Password : 1234 + +## Installation + +* `git clone ` +* Install Dependencies via `pip install -r 'requirements.txt'` +* `python manage.py makemigrations` +* `python manage.py migrate` +* `python manage.py runserver` + + +Note 01: Once user register on portal, admin should login [https://learn-live.herokuapp.com/admin/accounts/user/] and approve the account of that user. Only approved users can view our course content and participate in discussion forum. + +Note 02: All the Test Credentials are approved Users. So you can use them freely! + +## Features + +### Students + +- login and register +- enroll to available courses +- see all courses +- navigation to course where they can see different modules, contents of modules, announcements regarding the course. +- Chat with other students enrolled in that course through course chat room +- Participate in discussion forum with other teachers/parents/students + + +### Teachers +- Login and register +- Create multiple courses +- Edit/create new modules in each course +- Reorder modules whenever you feel necessary! +- Post course content in any way. It can be text/image/video/audio/ppt/pdf/etc... +- Participate in discussion forum with other teachers/parents/students +- Create Announcements in each course separately (Course Specific) + + +### Parents +- Login and register +- See announcements regarding their children's courses that are made by teachers +- Get list of all Events +- Get list of all Holidays +- Check progress of each student in every course (grades,attendance) +- Participate in discussion forum with other teachers/parents/students + +Note : Attendance and grades need to be registered by the admin once finalized. + + +## Technology Used +- Django +- Django Rest Framework +- Django Channels +- Tailblocks +- Javascript +- JQuery +- Html +- CSS + +We tried to follow the best coding principles while participating in this Code-Innovation-Series! + diff --git a/Lone_Developers/Lone_Developers/accounts/.gitignore b/Lone_Developers/Lone_Developers/accounts/.gitignore new file mode 100644 index 0000000..2e55d70 --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +migrations/ \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/accounts/__init__.py b/Lone_Developers/Lone_Developers/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/accounts/admin.py b/Lone_Developers/Lone_Developers/accounts/admin.py new file mode 100644 index 0000000..3017bcf --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from .models import * +from django.contrib.auth.admin import UserAdmin +class CustomUserAdmin(UserAdmin): + + fieldsets = UserAdmin.fieldsets + ( + ('Roles', {'fields': ('role','is_approved')}), + ) + add_fieldsets = UserAdmin.add_fieldsets + ( + ('Roles', {'fields': ('role',)}), + ) + +admin.site.register(User,CustomUserAdmin) +admin.site.register(Student) +admin.site.register(StudentToCourses) +admin.site.register(Parent) +admin.site.register(Teacher) +# Register your models here. diff --git a/Lone_Developers/Lone_Developers/accounts/apps.py b/Lone_Developers/Lone_Developers/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/Lone_Developers/Lone_Developers/accounts/models.py b/Lone_Developers/Lone_Developers/accounts/models.py new file mode 100644 index 0000000..507a81e --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/models.py @@ -0,0 +1,94 @@ +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import AbstractUser +from django.dispatch import receiver +#from django.http import HttpResponseBadRequest +from django.db.models.signals import post_save + + +# Create your models here. +from django.conf import settings +FEES = [1000*i for i in range(1,13)] + +class User(AbstractUser) : + class Role(models.TextChoices) : + STUDENT = "S" ,_("Student") + PARENT = "P" ,_("Parent") + TEACHER = "T",_("Teacher") + role = models.CharField(max_length = 255,choices=Role.choices) + is_approved= models.BooleanField(default=False) + +class Teacher(models.Model) : + user = models.OneToOneField(settings.AUTH_USER_MODEL,null=True,on_delete=models.CASCADE, related_name="teacher") + position = models.CharField(max_length=255,blank=True) + department = models.CharField(max_length=255,blank=True) + date_of_joining = models.DateField(default=timezone.now) + + def __str__(self): + return self.user.username + +class Parent(models.Model) : + user = models.OneToOneField(settings.AUTH_USER_MODEL,null=True,on_delete=models.CASCADE) + @property + def total_fees_to_be_paid(self) : + for child in self.children.all(): + fees+= child.fess + return fees + + def __str__(self): + return self.user.username + + + +class Student(models.Model) : + user = models.OneToOneField(settings.AUTH_USER_MODEL,null=True,on_delete=models.CASCADE, related_name="student") + roll_no = models.IntegerField(null=True) + parent = models.ForeignKey(Parent,related_name="children",null=True,on_delete=models.CASCADE) + standard = models.IntegerField(default =1) + is_fees_paid = models.BooleanField(default = False) + date_of_joining = models.DateField(default =timezone.now) + courses = models.ManyToManyField('courses.Course',related_name="student_courses",through="StudentToCourses", null=True) + + @property + def fees(self) : + return 0 if self.fess_is_paid else Fess[self.standard] + + def __str__(self): + return self.user.username + + + +class StudentToCourses(models.Model) : + class Grade(models.TextChoices) : + A = "A" ,_("A") + B = "B" ,_("B") + C = "C",_("C") + D = "D",_("D") + E = "E",_("E") + grade = models.CharField(max_length = 255,choices=Grade.choices, null=True, blank=True) + attendance = models.IntegerField(null=True) + student = models.ForeignKey(Student,related_name="reports",null=True,on_delete=models.CASCADE) + course = models.ForeignKey('courses.Course',related_name="report_cards",null=True,on_delete=models.CASCADE) + + + + + +@receiver(post_save,sender= User) +def generate_profile(sender,instance,created,**kwargs) : + role = instance.role + print(role) + + if created : + if role=="S" : + Student.objects.create(user=instance) + elif role=="T" : + Teacher.objects.create(user=instance) + elif role=="P" : + Parent.objects.create(user=instance) + + + + + diff --git a/Lone_Developers/Lone_Developers/accounts/static/images/log.png b/Lone_Developers/Lone_Developers/accounts/static/images/log.png new file mode 100644 index 0000000..bbdfb24 Binary files /dev/null and b/Lone_Developers/Lone_Developers/accounts/static/images/log.png differ diff --git a/Lone_Developers/Lone_Developers/accounts/templates/accounts/approve.html b/Lone_Developers/Lone_Developers/accounts/templates/accounts/approve.html new file mode 100644 index 0000000..f1e4cf4 --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/templates/accounts/approve.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + + Login +{% endblock %} + +{% block content %} +
+
+
+

Hi there, your username is "{{user.username}}"

+

Your registration is under process now .Once the verification is done + you can try logging in again .

+ + + +
+
+{% endblock %} \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/accounts/templates/accounts/login.html b/Lone_Developers/Lone_Developers/accounts/templates/accounts/login.html new file mode 100644 index 0000000..e7baa2c --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/templates/accounts/login.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + + Login +{% endblock %} + + +{% block content %} + +
+ +
+
+ + {% comment %}

Slow-carb next level shoindcgoitch ethical authentic, poko scenester

{% endcomment %} +
+ +
+
+ {% csrf_token %} +

Sign In

+ +
+ +

+
+
+ +
+
+ +{% endblock %} + diff --git a/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup.html b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup.html new file mode 100644 index 0000000..37e0499 --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} +Register +{% endblock %} +{% block content %} +
+
+
+

Sign Up

+

How would you like to register?

+
+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/parent.html b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/parent.html new file mode 100644 index 0000000..521395c --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/parent.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + + Parent Register +{% endblock %} + +{% block content %} +
+ +
+
+ + {% comment %}

Slow-carb next level shoindcgoitch ethical authentic, poko scenester

{% endcomment %} +
+ +
+
+ {% for message in messages%} + {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} +

{{ message }}

+ {% endif %} + {% endfor %} + {% csrf_token %} +

Sign Up

+ + + + + + seperate names by comma if multiples + + + + +
+ +
+
+ +
+
+ +{% endblock %} + diff --git a/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/student.html b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/student.html new file mode 100644 index 0000000..33db47b --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/student.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + + Student Register +{% endblock %} + +{% block content %} +
+ +
+
+ + {% comment %}

Slow-carb next level shoindcgoitch ethical authentic, poko scenester

{% endcomment %} +
+ +
+
+ {% csrf_token %} +

Sign Up

+ {% for message in messages%} + {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} +

{{ message }}

+ {% endif %} + {% endfor %} + + + + + + + + + + + + +
+ +
+
+ +
+
+ +{% endblock %} + diff --git a/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/teacher.html b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/teacher.html new file mode 100644 index 0000000..dd9b6e6 --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/templates/accounts/signup/teacher.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + + Teacher Register +{% endblock %} + +{% block content %} +
+ +
+
+ + {% comment %}

Slow-carb next level shoindcgoitch ethical authentic, poko scenester

{% endcomment %} +
+ +
+
+ {% for message in messages%} + {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} +

{{ message }}

+ {% endif %} + {% endfor %} + {% csrf_token %} +

Sign Up

+ + + + + + + + + + +
+ +
+
+ +
+
+ +{% endblock %} + diff --git a/Lone_Developers/Lone_Developers/accounts/tests.py b/Lone_Developers/Lone_Developers/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Lone_Developers/Lone_Developers/accounts/urls.py b/Lone_Developers/Lone_Developers/accounts/urls.py new file mode 100644 index 0000000..d21c92e --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/urls.py @@ -0,0 +1,31 @@ +"""hack_72 URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path +from .views import * + + +app_name="accounts" +urlpatterns = [ + path('login/', user_login, name='login'), + path('logout/', logout_user, name='logout'), + path('register/', register, name='register'), + path('student/', register_student, name='register_student'), + path('parent/', register_parent, name='register_parent'), + path('teacher/', register_teacher, name='register_teacher'), + path('approve/', approve, name='approve'), + +] diff --git a/Lone_Developers/Lone_Developers/accounts/views.py b/Lone_Developers/Lone_Developers/accounts/views.py new file mode 100644 index 0000000..a30fbbc --- /dev/null +++ b/Lone_Developers/Lone_Developers/accounts/views.py @@ -0,0 +1,176 @@ +from django.shortcuts import render,redirect +from accounts.models import User +from django.contrib.auth import authenticate,login,logout +from django.contrib import messages +from django.http import HttpResponse +from accounts.models import * +from django.http import HttpResponse +from django.views import View +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.models import AnonymousUser + +# Create your views here. + + + +def user_login(request) : + if request.method == "POST" : + username = request.POST['username'] + password = request.POST['password'] + user = authenticate(username = username,password=password) + + if user is not None and user.is_approved : + login(request,user) + role = user.role + + if role =="S" : + return redirect('students:student_course_list') + elif role=="T" : + return redirect('courses:manage_course_list') + elif role =="P" : + return redirect('parents:parent_index') + else : + messages.error(request,"Invalid credentials") + return render(request,"accounts/login.html") + elif user is not None and not user.is_approved: + login(request,user) + messages.info(request,"Approval needed") + return redirect('accounts:approve') + else : + messages.error(request,"Invalid credentials") + return render(request,"accounts/login.html") + + + else : + if request.user and not isinstance(request.user ,AnonymousUser) and request.user.is_approved: + role = request.user.role + if role =="S" : + return redirect('students:student_course_list') + elif role=="T" : + return redirect('courses:manage_course_list') + elif role =="P" : + return redirect('') + else : + messages.error(request,"Invalid credentials") + return render(request,"accounts/login.html") + elif request.user and not isinstance(request.user ,AnonymousUser) and not request.user.is_approved: + login(request,request.user) + messages.info(request,"Approval needed") + return redirect('accounts:approve') + + else : + return render(request,"accounts/login.html") + + +class Logout(LoginRequiredMixin,View): + + def get(self,request): + logout(request) + return redirect('accounts:login') + +logout_user = Logout.as_view() + + +def approve(request) : + return render(request,'accounts/approve.html') + + + +def register_student(request) : + if request.method =="POST" : + first_name =request.POST["first_name"] + last_name = request.POST["last_name"] + roll_no = request.POST["roll_no"] + password = request.POST["password"] + email=request.POST["password"] + standard = request.POST["standard"] + password_cnf = request.POST["confirm_password"] + username =first_name +str(roll_no)+str(standard) + + if User.objects.filter(username = username).exists() : + username = username+"_"+str(len(User.objects.filter(username = username))+1) + + if password != password_cnf : + messages.error(request,"Password Don't match") + return render(request,"accounts/signup/teacher.html") + + user = User.objects.create(first_name=first_name,last_name=last_name,password=password,email=email,role="S",username=username) + user.set_password(password) + user.save() + + user.student.roll_no = roll_no + user.student.standard= standard + user.student.save() + + return redirect("accounts:login") + + else : + return render(request,'accounts/signup/student.html') + + + +def register(request) : + return render(request,'accounts/signup.html') + + +def register_teacher(request) : + if request.method =="POST" : + first_name =request.POST["first_name"] + last_name = request.POST["last_name"] + department = request.POST["department"] + password = request.POST["password"] + password_cnf = request.POST["confirm_password"] + email=request.POST["email"] + username =first_name +last_name+department + + if password != password_cnf : + messages.error(request,"Password Don't match") + return render(request,"accounts/signup/teacher.html") + + if User.objects.filter(username = username).exists() : + username = username+"_"+str(len(User.objects.filter(username = username))+1) + user = User.objects.create(first_name=first_name,last_name=last_name,password=password,email=email,role="T",username=username) + user.set_password(password) + user.save() + + user.teacher.department =department + + user.teacher.save() + + return redirect("accounts:login") + + else : + return render(request,'accounts/signup/teacher.html') + + +def register_parent(request) : + if request.method =="POST" : + first_name =request.POST["first_name"] + last_name = request.POST["last_name"] + password = request.POST["password"] + email=request.POST["email"] + parent_of =request.POST["parent_of"] + password_cnf = request.POST["confirm_password"] + username =first_name+last_name + + if User.objects.filter(username = username).exists() : + username = username+"_"+str(len(User.objects.filter(username = username))+1) + + if password != password_cnf : + messages.error(request,"Password Don't match") + return render(request,"accounts/signup/teacher.html") + + user = User.objects.create(first_name=first_name,last_name=last_name,email=email,role="P",username=username) + user.set_password(password) + user.save() + child_list = list(parent_of.split(',')) + for c in child_list : + user.parent.children.add(User.objects.get(username=c).student) + + return redirect("accounts:login") + + else : + return render(request,'accounts/signup/parent.html') + + + diff --git a/Lone_Developers/Lone_Developers/chat/.gitignore b/Lone_Developers/Lone_Developers/chat/.gitignore new file mode 100644 index 0000000..2e55d70 --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +migrations/ \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/chat/__init__.py b/Lone_Developers/Lone_Developers/chat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/chat/admin.py b/Lone_Developers/Lone_Developers/chat/admin.py new file mode 100644 index 0000000..bba52a0 --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import * + + +admin.site.register(Message) +admin.site.register(Chat) +# Register your models here. diff --git a/Lone_Developers/Lone_Developers/chat/apps.py b/Lone_Developers/Lone_Developers/chat/apps.py new file mode 100644 index 0000000..7d4ea09 --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/apps.py @@ -0,0 +1,4 @@ +from django.apps import AppConfig + +class ChatConfig(AppConfig): + name = 'chat' diff --git a/Lone_Developers/Lone_Developers/chat/consumer.py b/Lone_Developers/Lone_Developers/chat/consumer.py new file mode 100644 index 0000000..2dcb34c --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/consumer.py @@ -0,0 +1,167 @@ +# chat/consumers.py +import json +from asgiref.sync import async_to_sync +from channels.generic.websocket import WebsocketConsumer +from chat.models import Message +from django.conf import settings +from .views import get_last_10_messages,get_curent_chat +# from channels.db import database_sync_to_sync +# from user.models import Message + +from accounts.models import User +#User=settings.AUTH_USER_MODEL + +class ChatConsumer(WebsocketConsumer): + + + def fetch_messages(self,data): + print('fetching') + messages=get_last_10_messages(int(self.room_name)) + context ={ + 'command': 'messages', + 'messages' : self.messages_to_json(messages,self.room_name) + } + self.send_message(context) + + + + def typing(self,data) : + person = User.objects.get(username=data['username']) + + context ={ + 'command':'typing', + 'type':data['type'], + 'message':{ + 'name':person.username + } + } + self.send_chat_message(context) + + + """ def online(self,data) : + person= User.objects.get(username=data['username']) + context ={ + 'command':'online', + 'message':{ + 'name':person.username + } + } + self.send_chat_message(context)""" + + + + + def new_messages(self,data) : + + user = User.objects.get(username =data["from"]) + + # author_user=User.objects.filter(username=contact)[0] + message = Message.objects.create(user=user,content=data['message']) + + content={ + 'command':'new_message', + 'message':self.message_to_json(message,self.room_name) + } + current_chat = get_curent_chat(self.room_name) + current_chat.messages.add(message) + current_chat.save() + + # print(data['message']) + + return self.send_chat_message(content) + + + def send_media(self,data) : + user = User.objects.get(username=data['from']) + content = { + "command":media , + "type" :data['type'], + "url" : data["url"] + } + self.send_chat_message(content) + + + def messages_to_json(self,messages,id) : + result = [] + for message in messages : + result.append(self.message_to_json(message,id)) + return result + + + def message_to_json(self,message,id): + return { + 'id':message.id, + 'author':message.user.username, + 'content':message.content, + 'timestamp':str(message.timestamp), + 'chatId':id + } + + commands ={ + 'fetch_messages': fetch_messages, + 'new_message' : new_messages, + # 'online':online, + 'typing':typing, + 'media':send_media + } + + def connect(self): + print("connecting") + self.room_name = self.scope['url_route']['kwargs']['room_name'] + self.room_group_name = 'chat_%s' % self.room_name + + # Join room group + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, + self.channel_name + ) + + self.accept() + + def disconnect(self, close_code): + # Leave room group + async_to_sync(self.channel_layer.group_discard)( + self.room_group_name, + self.channel_name + ) + # Receive message from WebSocket + + def receive(self, text_data): + data = json.loads(text_data) + self.commands[data['command']](self,data) + + + def send_chat_message(self,message) : + + #message =data_json['message'] + + # Send message to room group + + async_to_sync(self.channel_layer.group_send)( + self.room_group_name, + { + 'type': 'chat_message', + 'message': message + } + ) + print(self.room_group_name) + + + + + def send_message(self,context) : + + self.send(text_data=json.dumps(context + )) + + + # Receive message from room group + def chat_message(self, event): + # print('on chat.message worked') + message = event['message'] + + # Send message to WebSocket + self.send(text_data=json.dumps({ + 'message':message + } + )) diff --git a/Lone_Developers/Lone_Developers/chat/models.py b/Lone_Developers/Lone_Developers/chat/models.py new file mode 100644 index 0000000..57574a7 --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/models.py @@ -0,0 +1,48 @@ +from django.db import models +from django.conf import settings +from courses.models import Course +from django.dispatch import receiver +from django.db.models.signals import post_save,m2m_changed +from accounts.models import Student +from courses.models import Course + +User = settings.AUTH_USER_MODEL +# Create your models here. + +class Message(models.Model) : + user = models.ForeignKey(User,related_name="messages",on_delete=models.CASCADE) + timestamp = models.DateTimeField(auto_now_add=True) + content = models.TextField() + # read_by=models.ManyToManyField(Contact,related_name='messages_read') + + def __str__(self) : + return self.user.username + + +class Chat(models.Model) : + participants = models.ManyToManyField(User,related_name='chats') + messages = models.ManyToManyField(Message,blank=True,related_name='chat') + course = models.OneToOneField(Course ,related_name = "general_chat",on_delete=models.CASCADE) + + + + def __str__(self) : + + return "{}".format(self.pk) + + + def last_10_messages(self) : + return self.messages.objects.all().order_by('timestamp')[:10] + + + + +@receiver(post_save ,sender = Course) +def create_chat(sender,instance,created,**kwargs) : + if created : + chat =Chat.objects.create(course=instance) + chat.participants.set(instance.student_courses.all()) + chat.save() + + + diff --git a/Lone_Developers/Lone_Developers/chat/routing.py b/Lone_Developers/Lone_Developers/chat/routing.py new file mode 100644 index 0000000..f44ee7a --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/routing.py @@ -0,0 +1,7 @@ +from django.urls import re_path + +from . import consumer + +websocket_urlpatterns = [ + re_path(r'^ws/chat/(?P\w+)/$', consumer.ChatConsumer), +] diff --git a/Lone_Developers/Lone_Developers/chat/templates/chat/chat.html b/Lone_Developers/Lone_Developers/chat/templates/chat/chat.html new file mode 100644 index 0000000..6fdecf9 --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/templates/chat/chat.html @@ -0,0 +1,245 @@ + +{% load static %} + + + + + + + + + + Chat + + + + + + + + + + +
+
+
+
+ Participants +
+
+ + + {% for p in people %} +
  • +
    +
    + + +
    + +
    +
  • + {% endfor %} +
    +
    + +
    +
    +
    +
    +
    + + + +
    + + +
    +
    + +
    + +
    +
    +
    +
    + {{ room_name|json_script:"room-name" }} + + + + + + + + + diff --git a/Lone_Developers/Lone_Developers/chat/tests.py b/Lone_Developers/Lone_Developers/chat/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Lone_Developers/Lone_Developers/chat/urls.py b/Lone_Developers/Lone_Developers/chat/urls.py new file mode 100644 index 0000000..0d8773c --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/urls.py @@ -0,0 +1,25 @@ +"""hack_72 URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path +from django.conf import settings +from django.conf.urls.static import static +from .views import chat +app_name ="chat" +urlpatterns = [ + path('',chat,name="course_chat_room" ), +] + diff --git a/Lone_Developers/Lone_Developers/chat/views.py b/Lone_Developers/Lone_Developers/chat/views.py new file mode 100644 index 0000000..f94aa04 --- /dev/null +++ b/Lone_Developers/Lone_Developers/chat/views.py @@ -0,0 +1,33 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import render +import json +from django.utils.safestring import mark_safe +from django.shortcuts import get_object_or_404 +from .models import Chat +from accounts.models import User +from courses.models import Course +# from django.shortcuts import get_object_or_404 + + + +def get_curent_chat(chatId): + return get_object_or_404(Chat,id=chatId) + + +def get_last_10_messages(chatId): + + chat = get_object_or_404(Chat,id=chatId) + return chat.messages.order_by('timestamp')[:10] + +def chat(request, chat_id): + + chat = Chat.objects.get(id = int(chat_id)) + all_participants = chat.participants.all() + print(all_participants) + + return render(request, 'chat/chat.html', { + 'people':all_participants, + 'room_name':mark_safe(json.dumps(chat_id)), + 'username' : mark_safe(json.dumps(request.user.username or "")) + + }) diff --git a/Lone_Developers/Lone_Developers/courses/.gitignore b/Lone_Developers/Lone_Developers/courses/.gitignore new file mode 100644 index 0000000..2e55d70 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +migrations/ \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/courses/__init__.py b/Lone_Developers/Lone_Developers/courses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/courses/admin.py b/Lone_Developers/Lone_Developers/courses/admin.py new file mode 100644 index 0000000..ac18343 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from .models import Subject, Course, Module, Content, Text, File, Image, Video, Announcement, Discussion +from django.contrib import admin + +# use memcache admin index site +admin.site.index_template = 'memcache_status/admin_index.html' + +admin.site.register(Content) +admin.site.register(Text) +admin.site.register(File) +admin.site.register(Image) +admin.site.register(Video) +admin.site.register(Announcement) +admin.site.register(Discussion) + +@admin.register(Subject) +class SubjectAdmin(admin.ModelAdmin): + list_display = ['title', 'slug'] + prepopulated_fields = {'slug': ('title',)} + +class ModuleInline(admin.StackedInline): + model = Module + + +@admin.register(Course) +class CourseAdmin(admin.ModelAdmin): + list_display = ['title', 'subject', 'created', 'student_courses'] + list_filter = ['created', 'subject'] + search_fields = ['title', 'overview'] + prepopulated_fields = {'slug': ('title',)} + inlines = [ModuleInline] diff --git a/Lone_Developers/Lone_Developers/courses/api/__init__.py b/Lone_Developers/Lone_Developers/courses/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/courses/api/permissions.py b/Lone_Developers/Lone_Developers/courses/api/permissions.py new file mode 100644 index 0000000..db921d6 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/api/permissions.py @@ -0,0 +1,6 @@ +from rest_framework.permissions import BasePermission + + +class IsEnrolled(BasePermission): + def has_object_permission(self, request, view, obj): + return obj.students.filter(id=request.user.id).exists() diff --git a/Lone_Developers/Lone_Developers/courses/api/serializers.py b/Lone_Developers/Lone_Developers/courses/api/serializers.py new file mode 100644 index 0000000..173d290 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/api/serializers.py @@ -0,0 +1,53 @@ +from rest_framework import serializers +from ..models import Subject +from ..models import Course, Module, Content + + +class SubjectSerializer(serializers.ModelSerializer): + class Meta: + model = Subject + fields = ['id', 'title', 'slug'] + + +class ModuleSerializer(serializers.ModelSerializer): + class Meta: + model = Module + fields = ['order', 'title', 'description'] + + +class CourseSerializer(serializers.ModelSerializer): + modules = ModuleSerializer(many=True, read_only=True) + + class Meta: + model = Course + fields = ['id', 'subject', 'title', 'slug', 'overview', + 'created', 'owner', 'modules'] + + +class ItemRelatedField(serializers.RelatedField): + def to_representation(self, value): + return value.render() + + +class ContentSerializer(serializers.ModelSerializer): + item = ItemRelatedField(read_only=True) + + class Meta: + model = Content + fields = ['order', 'item'] + + +class ModuleWithContentsSerializer(serializers.ModelSerializer): + contents = ContentSerializer(many=True) + + class Meta: + model = Module + fields = ['order', 'title', 'description', 'contents'] + +class CourseWithContentsSerializer(serializers.ModelSerializer): + modules = ModuleWithContentsSerializer(many=True) + + class Meta: + model = Course + fields = ['id', 'subject', 'title', 'slug', + 'overview', 'created', 'owner', 'modules'] diff --git a/Lone_Developers/Lone_Developers/courses/api/urls.py b/Lone_Developers/Lone_Developers/courses/api/urls.py new file mode 100644 index 0000000..f3ca90e --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/api/urls.py @@ -0,0 +1,21 @@ +from django.urls import path, include +from rest_framework import routers +from . import views + +app_name = 'courses' + +router = routers.DefaultRouter() +router.register('courses', views.CourseViewSet) + +urlpatterns = [ + path('subjects/', + views.SubjectListView.as_view(), + name='subject_list'), + path('subjects//', + views.SubjectDetailView.as_view(), + name='subject_detail'), + # path('courses//enroll/', + # views.CourseEnrollView.as_view(), + # name='course_enroll'), + path('', include(router.urls)), +] diff --git a/Lone_Developers/Lone_Developers/courses/api/views.py b/Lone_Developers/Lone_Developers/courses/api/views.py new file mode 100644 index 0000000..dc745a2 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/api/views.py @@ -0,0 +1,53 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import generics +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.authentication import BasicAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework import viewsets +from rest_framework.decorators import action +from ..models import Subject, Course +from .serializers import SubjectSerializer, CourseSerializer, CourseWithContentsSerializer +from .permissions import IsEnrolled + + +class SubjectListView(generics.ListAPIView): + queryset = Subject.objects.all() + serializer_class = SubjectSerializer + + +class SubjectDetailView(generics.RetrieveAPIView): + queryset = Subject.objects.all() + serializer_class = SubjectSerializer + + +class CourseEnrollView(APIView): + authentication_classes = (BasicAuthentication,) + permission_classes = (IsAuthenticated,) + + def post(self, request, pk, format=None): + course = get_object_or_404(Course, pk=pk) + course.students.add(request.user) + return Response({'enrolled': True}) + + +class CourseViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Course.objects.all() + serializer_class = CourseSerializer + + @action(detail=True, + methods=['post'], + authentication_classes=[BasicAuthentication], + permission_classes=[IsAuthenticated]) + def enroll(self, request, *args, **kwargs): + course = self.get_object() + course.students.add(request.user) + return Response({'enrolled': True}) + + @action(detail=True, + methods=['get'], + serializer_class=CourseWithContentsSerializer, + authentication_classes=[BasicAuthentication], + permission_classes=[IsAuthenticated, IsEnrolled]) + def contents(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) diff --git a/Lone_Developers/Lone_Developers/courses/apps.py b/Lone_Developers/Lone_Developers/courses/apps.py new file mode 100644 index 0000000..a32e945 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoursesConfig(AppConfig): + name = 'courses' diff --git a/Lone_Developers/Lone_Developers/courses/fields.py b/Lone_Developers/Lone_Developers/courses/fields.py new file mode 100644 index 0000000..bcbc0de --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/fields.py @@ -0,0 +1,29 @@ +from django.db import models +from django.core.exceptions import ObjectDoesNotExist + + +class OrderField(models.PositiveIntegerField): + def __init__(self, for_fields=None, *args, **kwargs): + self.for_fields = for_fields + super().__init__(*args, **kwargs) + + def pre_save(self, model_instance, add): + if getattr(model_instance, self.attname) is None: + # no current value + try: + qs = self.model.objects.all() + if self.for_fields: + # filter by objects with the same field values + # for the fields in "for_fields" + query = {field: getattr(model_instance, field)\ + for field in self.for_fields} + qs = qs.filter(**query) + # get the order of the last item + last_item = qs.latest(self.attname) + value = last_item.order + 1 + except ObjectDoesNotExist: + value = 0 + setattr(model_instance, self.attname, value) + return value + else: + return super().pre_save(model_instance, add) diff --git a/Lone_Developers/Lone_Developers/courses/fixtures/subjects.json b/Lone_Developers/Lone_Developers/courses/fixtures/subjects.json new file mode 100644 index 0000000..7e65866 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/fixtures/subjects.json @@ -0,0 +1,34 @@ +[ +{ + "model": "courses.subject", + "pk": 1, + "fields": { + "title": "Mathematics", + "slug": "mathematics" + } +}, +{ + "model": "courses.subject", + "pk": 2, + "fields": { + "title": "Music", + "slug": "music" + } +}, +{ + "model": "courses.subject", + "pk": 3, + "fields": { + "title": "Physics", + "slug": "physics" + } +}, +{ + "model": "courses.subject", + "pk": 4, + "fields": { + "title": "Programming", + "slug": "programming" + } +} +] diff --git a/Lone_Developers/Lone_Developers/courses/forms.py b/Lone_Developers/Lone_Developers/courses/forms.py new file mode 100644 index 0000000..d3e1502 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/forms.py @@ -0,0 +1,11 @@ +from django import forms +from django.forms.models import inlineformset_factory +from .models import Course, Module + + +ModuleFormSet = inlineformset_factory(Course, + Module, + fields=['title', + 'description'], + extra=2, + can_delete=True) diff --git a/Lone_Developers/Lone_Developers/courses/models.py b/Lone_Developers/Lone_Developers/courses/models.py new file mode 100644 index 0000000..30c4ac2 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/models.py @@ -0,0 +1,109 @@ +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey +from django.template.loader import render_to_string +from .fields import OrderField +from django.conf import settings +# Create your models here. + + +class Subject(models.Model): + title = models.CharField(max_length=200) + slug = models.SlugField(max_length=200, unique=True) + + class Meta: + ordering = ['title'] + def __str__(self): + return self.title + + + +class Course(models.Model): + owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='courses_created', on_delete=models.CASCADE) + subject = models.ForeignKey(Subject, related_name='courses', on_delete=models.CASCADE) + title = models.CharField(max_length=200) + slug = models.SlugField(max_length=200, unique=True) + overview = models.TextField() + created = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created'] + + def __str__(self): + return self.title + + +class Announcement(models.Model): + course = models.ForeignKey(Course,related_name="announcements",on_delete=models.CASCADE) + content = models.TextField() + created = models.DateTimeField(auto_now_add=True) + + class Meta : + ordering = ['-created'] + +class Module(models.Model): + course = models.ForeignKey(Course, related_name='modules', on_delete=models.CASCADE) + title = models.CharField(max_length=200) + description = models.TextField(blank=True) + order = OrderField(blank=True, for_fields=['course']) + + class Meta: + ordering = ['order'] + + def __str__(self): + return f'{self.order}. {self.title}' + +class Content(models.Model): + module = models.ForeignKey(Module, related_name='contents', on_delete=models.CASCADE) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, + limit_choices_to={'model__in':( 'text','video','image','file')}) + object_id = models.PositiveIntegerField() + item = GenericForeignKey('content_type', 'object_id') + order = OrderField(blank=True, for_fields=['module']) + + class Meta: + ordering = ['order'] + +class ItemBase(models.Model): + owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='%(class)s_related', on_delete=models.CASCADE) + title = models.CharField(max_length=250) + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + + def __str__(self): + return self.title + + def render(self): + return render_to_string(f'courses/content/{self._meta.model_name}.html', + {'item': self}) + + + class Meta: + abstract = True + +class Text(ItemBase): + content = models.TextField() + +class File(ItemBase): + file = models.FileField(upload_to='files') + +class Image(ItemBase): + file = models.FileField(upload_to='images') + +class Video(ItemBase): + url = models.URLField() + + +class Discussion(models.Model): + title = models.CharField(max_length=100) + created = models.DateTimeField(auto_now_add=True, null=True) + content = models.TextField(null=True) + file = models.FileField(upload_to='discussion/') + user = models.ForeignKey(settings.AUTH_USER_MODEL,related_name="discussion",on_delete=models.CASCADE) + + def __str__(self): + return self.title + + class Meta : + ordering =['-created'] \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/courses/static/css/base.css b/Lone_Developers/Lone_Developers/courses/static/css/base.css new file mode 100644 index 0000000..09bf5cc --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/static/css/base.css @@ -0,0 +1,321 @@ +@import url(//fonts.googleapis.com/css?family=Roboto:500,300,400); + +body { + margin:0; + font-family:'Roboto', sans-serif; + font-weight:400; + color:#333; +} + +a { + color:#3fad37; + text-decoration:none; +} + +ul { + float:left; +} + +h1, h2, h3, h4, h5, h6 { + font-family:'Roboto', sans-serif; + font-weight:300; +} + +h1 { + background:#efefef; + width:100%; + overflow:auto; + padding:20px 0 20px 40px; + margin:0; +} + +.my-module { + padding:10px 20px; + float:left; + width:600px; +} + +.mod-res{ + padding:10px 20px; +} + + +.my-module h3,.mod-res h3 { + margin:20px 0 0; + padding:0; + width:100%; +} + +.my-module p,.mod-res p { + margin:10px 0 20px; + width:100%; + float:left; +} + +#header { + background:#4dcc43; + overflow:auto; + padding:10px 20px; + border-bottom:6px solid #3fad37; +} + +#header .logo { + text-decoration:none; + font-family:'Roboto', sans-serif; + font-weight:300; + text-transform:uppercase; + font-size:24px; + color:#fff; + float:left; +} + +#header a { + color:#fff; +} + +#header .menu { + list-style:none; + float:right; + margin:0; + padding:0; +} +.contents-res{ + + padding:10px; + + background:#333; + color: #fff; + font-family:'Roboto', sans-serif; +} +.my-contents { + width:20%; + padding:10px; + float:left; + background:#333; + color: #fff; + font-family:'Roboto', sans-serif; +} + +.my-contents ul,.contents-res ul { + list-style:none; + margin:0; + padding:0; + width:100%; +} + +.my-contents ul li ,.contents-res ul li{ + margin:0 0 10px 0; + padding:4px 10px 10px; + overflow:auto; + cursor:move; +} + +.my-contents ul li.selected,.contents-res ul li.selected { + background:#3f3f3f; +} + +.my-contents ul li:hover,.contents-res ul li:hover { + background:#3f3f3f; +} + +.my-contents ul span,.contents-res ul span{ + text-transform:uppercase; + color:#bbb; + font-size:14px; +} + +.my-contents ul a { + display:block; + color:#fff; + text-decoration:none; +} + +.my-contents ul a:hover, .contents-res ul a:hover{ + color:#4dcc43; + cursor:move; +} + +ul.content-types li { + list-style:none; + float:left; + margin:10px; + background:#efefef; + padding:8px 14px; +} + +.hidden { + display:none; +} + +form p { + overflow:auto; +} + +.errorlist { + color:#ae2c2c; + margin:0; +} + +label { + float:left; + clear:both; + margin:0 0 8px 0; +} + +input, select, textarea { + border:1px solid #ccc; + border-bottom:3px solid #ccc; + padding:8px 12px; + font-size:16px; + font-family:'Roboto', sans-serif; + float:left; + clear:both; + width:300px; +} + +textarea { + height:80px; +} + +select { + width:324px; +} + +input[type=submit], a.button { + border-radius:5px; + background:#4dcc43; + color:#fff; + font-size:16px; + text-transform:uppercase; + border:none; + padding:10px 20px; + margin:20px 0; +} + +a.secondary-button { + border:3px solid #4dcc43; + padding:10px 20px; + margin:10px 0; +} + +input[type=submit]:hover, a.button:hover { + background:#3fad37; +} + +ul#course-modules { + list-style:none; + overflow:auto; +} + +ul#course-modules textarea { + width:600px; + height:120px; +} + +ul#course-modules li { + padding:20px; + overflow:auto; + cursor:move; +} + +ul#course-modules li:nth-child(even) { + background:#efefef; +} + +ul#course-modules li:hover { + background:#ccc; +} + +#module-contents div { + padding:10px 20px; + border:1px solid #ccc; + background:#fff +} + +#module-contents form { + margin:0; + padding:0; +} + +#module-contents input[type=submit] { + color:#3fad37; + background:none; + margin:-20px 0 0; + padding:0; + float:left; + text-transform:none; +} + +#module-contents div:hover { + cursor:move; +} + +.course-info { + border:1px solid #ccc; + padding:0 20px; + margin-bottom:10px; + width:400px; + overflow:auto; +} + +.course-info a { + margin-right:10px; +} + +.helptext { + color:#ccc; + padding-left:20px; +} + +#chat { + top: 64px; + bottom:0; + position:fixed; + width:100%; + overflow-y:scroll; + padding-bottom:150px; +} + +#chat .message { + background:#efefef; + padding:10px 20px; + border-radius:4px; + width:auto; + display:inline; + float:left; + margin:10px 10px 0; + min-width:440px; + clear:both; +} + +#chat .message.me { + display:inline; + float:right; + background:#DCEDE0; + color:#56A668; +} + +#chat .date { + color:#aaa; + font-style:italic; + font-size:12px; +} + +#chat-input { + position:absolute; + bottom:0; + background:#efefef; + width:100%; + padding-top:20px; +} + +#chat-input input { + width:96%; + position:left; + float:left; + display:inline; + margin-left:2%; + margin-right:2%; + padding-left:0; + padding-right:0; +} diff --git a/Lone_Developers/Lone_Developers/courses/templates/base.html b/Lone_Developers/Lone_Developers/courses/templates/base.html new file mode 100644 index 0000000..725cba6 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/base.html @@ -0,0 +1,58 @@ +{% load static %} + + + + + {% block title %}E-Learning Platform{% endblock %} + + + + +
    +
    + + + + + E-Learning Platform + + + {% comment %} {% endcomment %} +
    +
    + + +
    + {% block content %} + {% endblock %} +
    + + + + + + diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/announcement/create_announcement.html b/Lone_Developers/Lone_Developers/courses/templates/courses/announcement/create_announcement.html new file mode 100644 index 0000000..2039dac --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/announcement/create_announcement.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + + Create Announcment +{% endblock %} + +{% block content %} +

    + {% if object %} + course "{{ object.course.title }}" + {% else %} + Create Announcment + {% endif %} +

    +
    +
    +
    + +
    + {% for field in form %} +
    + + {{ field|add_classes:"shadow appearance-none border mb-3 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" }} +
    + {% endfor %} + + {% csrf_token %} +
    + + +
    + +
    + +
    +
    +
    +{% endblock %} + diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/content/file.html b/Lone_Developers/Lone_Developers/courses/templates/courses/content/file.html new file mode 100644 index 0000000..28d3a40 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/content/file.html @@ -0,0 +1 @@ +

    Download file

    diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/content/image.html b/Lone_Developers/Lone_Developers/courses/templates/courses/content/image.html new file mode 100644 index 0000000..9cd3057 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/content/image.html @@ -0,0 +1 @@ +

    {{ item.title }}

    diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/content/text.html b/Lone_Developers/Lone_Developers/courses/templates/courses/content/text.html new file mode 100644 index 0000000..c143f3a --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/content/text.html @@ -0,0 +1 @@ +{{ item.content|linebreaks }} diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/content/video.html b/Lone_Developers/Lone_Developers/courses/templates/courses/content/video.html new file mode 100644 index 0000000..5897aba --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/content/video.html @@ -0,0 +1,2 @@ +{% load embed_video_tags %} +{% video item.url "small" %} diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/course/detail.html b/Lone_Developers/Lone_Developers/courses/templates/courses/course/detail.html new file mode 100644 index 0000000..46bd467 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/course/detail.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block title %} + {{ object.title }} +{% endblock %} + +{% block content %} + {% with subject=object.subject %} +

    + {{ object.title }} +

    +
    +

    Overview

    +

    + + {{ subject.title }}. + {{ object.modules.count }} modules. + Instructor: {{ object.owner.get_full_name }} +

    + {{ object.overview|linebreaks }} + {% if request.user.is_authenticated %} +
    + {{ enroll_form }} + {% csrf_token %} + +
    + {% else %} + + Register to enroll + + {% endif %} +
    + {% endwith %} +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/course/list.html b/Lone_Developers/Lone_Developers/courses/templates/courses/course/list.html new file mode 100644 index 0000000..f4f9bde --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/course/list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block title %} + {% if subject %} + {{ subject.title }} courses + {% else %} + All courses + {% endif %} +{% endblock %} + +{% block content %} +

    + {% if subject %} + {{ subject.title }} courses + {% else %} + All courses + {% endif %} +

    +
    +

    Subjects

    + +
    +
    + {% for course in courses %} + {% with subject=course.subject %} +

    + + {{ course.title }} + +

    +

    + {{ subject }}. + {{ course.total_modules }} modules. + Instructor: {{ course.owner.get_full_name }} +

    + {% endwith %} + {% endfor %} +
    +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/manage/content/form.html b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/content/form.html new file mode 100644 index 0000000..7f5bb8e --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/content/form.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %} + {% if object %} + Edit content "{{ object.title }}" + {% else %} + Add new content + {% endif %} +{% endblock %} + +{% block content %} +
    +
    +

    + {% if object %} + Edit content "{{ object.title }}" + {% else %} + Add new content + {% endif %} +

    +
    +

    Course info

    +
    + {{ form.as_p }} + {% csrf_token %} +

    +
    + + +
    + +
    +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/delete.html b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/delete.html new file mode 100644 index 0000000..b91697d --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/delete.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %}Delete course{% endblock %} + +{% block content %} +
    +
    +

    Delete course "{{ object.title }}"

    + +
    +
    + {% csrf_token %} +

    Are you sure you want to delete "{{ object.title }}"?

    + +
    +
    + +
    + +
    +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/form.html b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/form.html new file mode 100644 index 0000000..eb8c581 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/form.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + {% if object %} + Edit course "{{ object.title }}" + {% else %} + Create a new course + {% endif %} +{% endblock %} + +{% block content %} +

    + {% if object %} + Edit course "{{ object.title }}" + {% else %} + Create a new course + {% endif %} +

    +
    +
    +
    +

    Course info

    +
    + {% for field in form %} +
    + + {{ field|add_classes:"shadow appearance-none border mb-3 rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" }} +
    + {% endfor %} + + {% csrf_token %} +
    + + + + +
    +
    +
    +{% endblock %} + diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/list.html b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/list.html new file mode 100644 index 0000000..2a6deed --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/course/list.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} + +{% block title %}My courses{% endblock %} + +{% block content %} + + {% comment %}
    +
    +

    Hi {{request.user}}!, Below are your current courses

    + + Create new course + +
    +
    {% endcomment %} + +
    +
    +

    You are logged in as teacher

    +

    Hi {{request.user}}, You are teaching below courses

    +

    ‘It takes a big heart to help shape little minds’

    +
    + +
    +
    + +
    +
    + {% for course in object_list %} +
    +
    +
    + + + +
    +

    {{ course.title}}

    +

    {{course.overview}}

    +

    Number of Students Enrolled : {{course.student_courses.all|length}}

    +

    + Create Announcment + +

    +

    + Edit Course Details + +

    +

    + Edit Modules + +

    + {% if course.modules.count > 0 %} +

    + Manage Module contents + +

    + {% endif %} +

    + Delete Course + +

    +
    +
    + {% empty %} +

    You haven't created any courses yet.

    + Create new course + {% endfor %} + +
    + + + + +
    +
    + + + + + + + +
    +{% endblock %} + + + diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/manage/module/content_list.html b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/module/content_list.html new file mode 100644 index 0000000..4b69b28 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/module/content_list.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} +{% load course %} + +{% block title %} + Module {{ module.order|add:1 }}: {{ module.title }} +{% endblock %} + +{% block content %} + {% with course=module.course %} +

    + Go Back +

    +
    +
    +

    Modules for {{course.title}}

    + +

    + Edit modules

    + +
    +
    +

    Module {{ module.order|add:1 }}: {{ module.title }}

    +

    Module contents:

    + +
    + {% for content in module.contents.all %} +
    + {% with item=content.item %} +

    {{ item }} ({{ item|model_name }})

    + + Edit + +
    + + {% csrf_token %} +
    + {% endwith %} +
    + {% empty %} +

    This module has no contents yet.

    + {% endfor %} + +
    +

    Add new content:

    + +
    +
    + +

    Announcements

    +
    + {% for a in announcements %} +
    +

    {{a.content}}

    +

    - {{ a.created }}

    +
    +
    + {% endfor %} +
    +
    + {% endwith %} +{% endblock %} + +{% block domready %} + $('#modules').sortable({ + stop: function(event, ui) { + modules_order = {}; + $('#modules').children().each(function(){ + // update the order field + $(this).find('.order').text($(this).index() + 1); + // associate the module's id with its order + modules_order[$(this).data('id')] = $(this).index(); + }); + $.ajax({ + type: 'POST', + url: '{% url "courses:module_order" %}', + contentType: 'application/json; charset=utf-8', + dataType: 'json', + data: JSON.stringify(modules_order) + }); + } + }); + + $('#module-contents').sortable({ + stop: function(event, ui) { + contents_order = {}; + $('#module-contents').children().each(function(){ + // associate the module's id with its order + contents_order[$(this).data('id')] = $(this).index(); + }); + + $.ajax({ + type: 'POST', + url: '{% url "courses:content_order" %}', + contentType: 'application/json; charset=utf-8', + dataType: 'json', + data: JSON.stringify(contents_order), + }); + } + }); +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/courses/templates/courses/manage/module/formset.html b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/module/formset.html new file mode 100644 index 0000000..9288aca --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/courses/manage/module/formset.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} +{% load course %} +{% block title %} + Edit "{{ course.title }}" +{% endblock %} + +{% block content %} +

    Edit "{{ course.title }}"

    + + +
    +
    + + +
    +
    +
    + + + {{ formset.management_form }} + +
    + {% for form in formset %} +
    +
    + + {{form}} +
    +
    + + {% endfor %} + +
    +
    + + {% csrf_token %} +
    +
    +
    + +{% endblock %} + + + + + + + +
    +
    +
    + + +
    + +
    +
    +
    +
    + diff --git a/Lone_Developers/Lone_Developers/courses/templates/discussion.html b/Lone_Developers/Lone_Developers/courses/templates/discussion.html new file mode 100644 index 0000000..b093ed8 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/discussion.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block content%} + +
    +

    Discussion Forum

    +
    + {% for u in uploads %} + +
    +
    + {{u.user.username}} + {{u.created|date:"M d, Y"}} +
    +
    +

    {{u.title}}

    +

    {{u.content}}

    + Learn More + + + + + +
    +
    + {% endfor %} + + + +
    + +
    + +
    +
    +
    + {% csrf_token %} +
    +

    Upload Content

    +

    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/courses/templates/index.html b/Lone_Developers/Lone_Developers/courses/templates/index.html new file mode 100644 index 0000000..f03ed58 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templates/index.html @@ -0,0 +1,175 @@ + + + + + + E-Learning Platform + + + +
    +
    + + + + + E-Learning Platform + + + {% if not request.user.is_authenticated%} + + {% endif %} +
    + +
    +
    +
    + hero +
    +
    +

    Start your Online Learning With Us! + +

    +

    With rapid advancement in technology, it is now possible for learners to study from anywhere around the world. We ensure the quality of teaching is never compromised and thus bring the courses from the best educators in the field.

    +
    + {% if not request.user.is_authenticated%} + {% elif request.user.role == "S" %} + + {% endif%} + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + blog +
    +

    CATEGORY

    +

    Online Education

    +

    If you are a teacher, student, parent, or administrator, you should be following education blogs. Why? Simply because blogs are an ever-increasing way to spark ideas, creativity, and innovation.

    + +
    +
    +
    +
    +
    + blog +
    +

    CATEGORY

    +

    Student and Alumni Linkage

    +

    Student Alumni Interaction Linkage (SAIL) is a voluntary cell of IIT Guwahati under the office of Alumni Affairs and External Relations.

    + +
    +
    +
    +
    +
    + blog +
    +

    CATEGORY

    +

    Advantages of Learning Online

    +

    For decades, education was relegated to stifling classrooms with blackboards and uncomfortable desks. Today, students of all ages and experience levels have far more options.

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + {% comment %}
    +
    +

    ADDRESS

    +

    IIT Guwahati

    +
    +
    +

    EMAIL

    + example@email.com +

    PHONE

    +

    123-456-7890

    +
    +
    {% endcomment %} +
    +
    +

    Feedback

    +

    We are happy to hear from you

    + + + + +
    +
    +
    + + \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/courses/templatetags/__init__.py b/Lone_Developers/Lone_Developers/courses/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/courses/templatetags/course.py b/Lone_Developers/Lone_Developers/courses/templatetags/course.py new file mode 100644 index 0000000..59448de --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/templatetags/course.py @@ -0,0 +1,44 @@ +from django import template +from django.urls import reverse + +register = template.Library() + +@register.filter +def model_name(obj): + try: + return obj._meta.model_name + except AttributeError: + return None + + +@register.filter('input_type') +def input_type(ob): + ''' + Extract form field type + :param ob: form field + :return: string of form field widget type + ''' + return ob.field.widget.__class__.__name__ + + +@register.filter(name='add_classes') +def add_classes(value, arg): + ''' + Add provided classes to form field + :param value: form field + :param arg: string of classes seperated by ' ' + :return: edited field + ''' + css_classes = value.field.widget.attrs.get('class', '') + # check if class is set or empty and split its content to list (or init list) + if css_classes: + css_classes = css_classes.split(' ') + else: + css_classes = [] + # prepare new classes to list + args = arg.split(' ') + for a in args: + if a not in css_classes: + css_classes.append(a) + # join back to single string + return value.as_widget(attrs={'class': ' '.join(css_classes)}) diff --git a/Lone_Developers/Lone_Developers/courses/tests.py b/Lone_Developers/Lone_Developers/courses/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Lone_Developers/Lone_Developers/courses/urls.py b/Lone_Developers/Lone_Developers/courses/urls.py new file mode 100644 index 0000000..e47e395 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/urls.py @@ -0,0 +1,60 @@ +from django.urls import path +from . import views +app_name = "courses" +urlpatterns = [ + path('mine/', + views.ManageCourseListView.as_view(), + name='manage_course_list'), + + path('create/', + views.CourseCreateView.as_view(), + name='course_create'), + + path('/edit/', + views.CourseUpdateView.as_view(), + name='course_edit'), + + path('/delete/', + views.CourseDeleteView.as_view(), + name='course_delete'), + + path('/module/', + views.CourseModuleUpdateView.as_view(), + name='course_module_update'), + + path('module//content//create/', + views.ContentCreateUpdateView.as_view(), + name='module_content_create'), + + path('module//content///', + views.ContentCreateUpdateView.as_view(), + name='module_content_update'), + + path('content//delete/', + views.ContentDeleteView.as_view(), + name='module_content_delete'), + + path('module//', + views.ModuleContentListView.as_view(), + name='module_content_list'), + + path('module/order/', + views.ModuleOrderView.as_view(), + name='module_order'), + + path('content/order/', + views.ContentOrderView.as_view(), + name='content_order'), + + path('subject//', + views.CourseListView.as_view(), + name='course_list_subject'), + + path('/', + views.CourseDetailView.as_view(), + name='course_detail'), + + path('/create_announcement/', + views.AnnouncementCreateView.as_view(), + name='new_announcement'), +] diff --git a/Lone_Developers/Lone_Developers/courses/views.py b/Lone_Developers/Lone_Developers/courses/views.py new file mode 100644 index 0000000..27f44a4 --- /dev/null +++ b/Lone_Developers/Lone_Developers/courses/views.py @@ -0,0 +1,259 @@ + +from django.urls import reverse_lazy +from django.shortcuts import render,redirect, get_object_or_404 +from django.views.generic.base import TemplateResponseMixin, View +from django.views.generic.list import ListView +from django.views.generic.edit import CreateView, UpdateView, \ + DeleteView +from django.views.generic.detail import DetailView +from django.contrib.auth.mixins import LoginRequiredMixin, \ + PermissionRequiredMixin +from django.forms.models import modelform_factory +from django.apps import apps +from django.db.models import Count +from django.core.cache import cache +from braces.views import CsrfExemptMixin, JsonRequestResponseMixin +from students.forms import CourseEnrollForm +from .models import Course, Module, Content, Subject, Announcement, Discussion +from .forms import ModuleFormSet + +def home_view(request): + return render(request,'index.html') + + +class OwnerMixin(object): + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(owner=self.request.user) + + +class OwnerEditMixin(object): + def form_valid(self, form): + form.instance.owner = self.request.user + return super().form_valid(form) + + +class OwnerCourseMixin(OwnerMixin, + LoginRequiredMixin): + model = Course + fields = ['subject', 'title', 'slug', 'overview'] + success_url = reverse_lazy('courses:manage_course_list') + + +class OwnerCourseEditMixin(OwnerCourseMixin, OwnerEditMixin): + template_name = 'courses/manage/course/form.html' + + +class ManageCourseListView(OwnerCourseMixin, ListView): + template_name = 'courses/manage/course/list.html' + +class CourseCreateView(OwnerCourseEditMixin, CreateView): + pass + + +class CourseUpdateView(OwnerCourseEditMixin, UpdateView): + pass + + +class CourseDeleteView(OwnerCourseMixin, DeleteView): + template_name = 'courses/manage/course/delete.html' + + + +class CourseModuleUpdateView(TemplateResponseMixin, View): + template_name = 'courses/manage/module/formset.html' + course = None + + def get_formset(self, data=None): + return ModuleFormSet(instance=self.course, + data=data) + + def dispatch(self, request, pk): + self.course = get_object_or_404(Course, + id=pk, + owner=request.user) + return super().dispatch(request, pk) + + def get(self, request, *args, **kwargs): + formset = self.get_formset() + return self.render_to_response({'course': self.course, + 'formset': formset}) + + def post(self, request, *args, **kwargs): + formset = self.get_formset(data=request.POST) + print(formset) + if formset.is_valid(): + formset.save() + return redirect('courses:manage_course_list') + + return self.render_to_response({'course': self.course, + 'formset': formset}) + + +class ContentCreateUpdateView(TemplateResponseMixin, View): + module = None + model = None + obj = None + template_name = 'courses/manage/content/form.html' + + def get_model(self, model_name): + if model_name in ['text', 'video', 'image', 'file']: + return apps.get_model(app_label='courses', + model_name=model_name) + return None + + def get_form(self, model, *args, **kwargs): + Form = modelform_factory(model, exclude=['owner', + 'order', + 'created', + 'updated']) + return Form(*args, **kwargs) + + def dispatch(self, request, module_id, model_name, id=None): + self.module = get_object_or_404(Module, + id=module_id, + course__owner=request.user) + self.model = self.get_model(model_name) + if id: + self.obj = get_object_or_404(self.model, + id=id, + owner=request.user) + return super().dispatch(request, module_id, model_name, id) + + def get(self, request, module_id, model_name, id=None): + form = self.get_form(self.model, instance=self.obj) + return self.render_to_response({'form': form, + 'object': self.obj}) + + def post(self, request, module_id, model_name, id=None): + form = self.get_form(self.model, + instance=self.obj, + data=request.POST, + files=request.FILES) + if form.is_valid(): + obj = form.save(commit=False) + obj.owner = request.user + obj.save() + if not id: + # new content + Content.objects.create(module=self.module, + item=obj) + return redirect('courses:module_content_list', self.module.id) + + return self.render_to_response({'form': form, + 'object': self.obj}) + + +class ContentDeleteView(View): + def post(self, request, id): + content = get_object_or_404(Content, + id=id, + module__course__owner=request.user) + module = content.module + content.item.delete() + content.delete() + return redirect('courses:module_content_list', module.id) + + +class ModuleContentListView(TemplateResponseMixin, View): + template_name = 'courses/manage/module/content_list.html' + + def get(self, request, module_id): + module = get_object_or_404(Module, + id=module_id, + course__owner=request.user) + + return self.render_to_response({'module': module,'announcements':module.course.announcements.all()}) + + +class ModuleOrderView(CsrfExemptMixin, + JsonRequestResponseMixin, + View): + def post(self, request): + for id, order in self.request_json.items(): + Module.objects.filter(id=id, + course__owner=request.user).update(order=order) + return self.render_json_response({'saved': 'OK'}) + + +class ContentOrderView(CsrfExemptMixin, + JsonRequestResponseMixin, + View): + def post(self, request): + for id, order in self.request_json.items(): + Content.objects.filter(id=id, + module__course__owner=request.user) \ + .update(order=order) + return self.render_json_response({'saved': 'OK'}) + + +class CourseListView(TemplateResponseMixin, View): + model = Course + template_name = 'courses/course/list.html' + + def get(self, request, subject=None): + subjects = cache.get('all_subjects') + if not subjects: + subjects = Subject.objects.annotate( + total_courses=Count('courses')) + cache.set('all_subjects', subjects) + all_courses = Course.objects.annotate( + total_modules=Count('modules')) + if subject: + subject = get_object_or_404(Subject, slug=subject) + key = f'subject_{subject.id}_courses' + courses = cache.get(key) + if not courses: + courses = all_courses.filter(subject=subject) + cache.set(key, courses) + else: + courses = cache.get('all_courses') + if not courses: + courses = all_courses + cache.set('all_courses', courses) + return self.render_to_response({'subjects': subjects, + 'subject': subject, + 'courses': courses}) + +class AnnouncementCreateView(CreateView) : + model = Announcement + template_name = 'courses/announcement/create_announcement.html' + fields = ['content'] + success_url = reverse_lazy('courses:manage_course_list') + + def form_valid(self,form) : + course = Course.objects.get(pk=self.kwargs['pk']) + form.instance.course = course + return super(AnnouncementCreateView,self).form_valid(form) + + +class CourseDetailView(DetailView): + model = Course + template_name = 'courses/course/detail.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['enroll_form'] = CourseEnrollForm( + initial={'course':self.object}) + context['announcements'] = self.object.announcements.all() + return context + + + +class DiscussionPage(LoginRequiredMixin,View) : + + + def get(self,request,**kwargs) : + uploads = Discussion.objects.all() + return render(request,'discussion.html',{'uploads':uploads}) + + def post(self,request,**kwargs) : + file = request.FILES['file'] + title =request.POST['title'] + description = request.POST['description'] + upload = Discussion.objects.create(file=file,title=title,user=request.user, content=description) + + return redirect('discussion_page') + + + diff --git a/Lone_Developers/Lone_Developers/db.sqlite3 b/Lone_Developers/Lone_Developers/db.sqlite3 new file mode 100644 index 0000000..4cf9c47 Binary files /dev/null and b/Lone_Developers/Lone_Developers/db.sqlite3 differ diff --git a/Lone_Developers/Lone_Developers/hack72/.gitignore b/Lone_Developers/Lone_Developers/hack72/.gitignore new file mode 100644 index 0000000..2e55d70 --- /dev/null +++ b/Lone_Developers/Lone_Developers/hack72/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +migrations/ \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/hack72/__init__.py b/Lone_Developers/Lone_Developers/hack72/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/hack72/asgi.py b/Lone_Developers/Lone_Developers/hack72/asgi.py new file mode 100644 index 0000000..4eaa36f --- /dev/null +++ b/Lone_Developers/Lone_Developers/hack72/asgi.py @@ -0,0 +1,17 @@ +""" +ASGI config for hack72 project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ +""" + +import os +import django +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hack72.settings') + +django.setup() +application = get_asgi_application() diff --git a/Lone_Developers/Lone_Developers/hack72/routing.py b/Lone_Developers/Lone_Developers/hack72/routing.py new file mode 100644 index 0000000..51a6e6c --- /dev/null +++ b/Lone_Developers/Lone_Developers/hack72/routing.py @@ -0,0 +1,11 @@ +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +import chat.routing + +application = ProtocolTypeRouter({ + 'websocket': AuthMiddlewareStack( + URLRouter( + chat.routing.websocket_urlpatterns + ) + ), +}) diff --git a/Lone_Developers/Lone_Developers/hack72/settings.py b/Lone_Developers/Lone_Developers/hack72/settings.py new file mode 100644 index 0000000..ea8e4ad --- /dev/null +++ b/Lone_Developers/Lone_Developers/hack72/settings.py @@ -0,0 +1,166 @@ +""" +Django settings for educa project. + +Generated by 'django-admin startproject' using Django 3.0. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.0/ref/settings/ + +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '=$z#p(r5okulew34y&aju1bw+&_#$)5xgyzh(5c7)6$0&qyay0' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'courses.apps.CoursesConfig', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'students.apps.StudentsConfig', + 'embed_video', + 'memcache_status', + 'rest_framework', + 'chat', + 'channels', + 'accounts', + 'parent', + +] + +MIDDLEWARE = [ + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + # 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.cache.FetchFromCacheMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'hack72.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'hack72.wsgi.application' + + +ASGI_APPLICATION = 'hack72.routing.application' +CHANNEL_LAYERS = { + 'default': { + + # the chat messages will stored in the redis server + +# 'BACKEND': 'channels_redis.core.RedisChannelLayer', +# 'CONFIG': { +# "hosts": [('127.0.0.1', 6379)], +# #localhost,port +# }, + "BACKEND": "channels.layers.InMemoryChannelLayer", + }, +} +# REDIS_HOST = 'localhost' +# REDIS_PORT = 6379 + +# Database +# https://docs.djangoproject.com/en/3.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} +AUTH_USER_MODEL = 'accounts.User' + + +# Password validation +# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.0/howto/static-files/ + +STATIC_URL = '/static/' + +STATICFILES_DIRS=[ + os.path.join(BASE_DIR,'static') +] +STATIC_ROOT =os.path.join(BASE_DIR,'assets') +#MEDIA FILES +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + + +MEDIA_URL = '/media/' +MEDIA_ROOT =os.path.join(BASE_DIR,'media') +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.1/howto/static-files/ + diff --git a/Lone_Developers/Lone_Developers/hack72/urls.py b/Lone_Developers/Lone_Developers/hack72/urls.py new file mode 100644 index 0000000..1a9d30b --- /dev/null +++ b/Lone_Developers/Lone_Developers/hack72/urls.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from courses.views import CourseListView, home_view, DiscussionPage + +urlpatterns = [ + + path('admin/', admin.site.urls), + path('course/', include('courses.urls')), + path('', home_view, name='home'), + path('students/', include('students.urls')), + path('parents/', include('parent.urls')), + path('accounts/', include('accounts.urls')), + path('api/', include('courses.api.urls', namespace='api')), + path('chat/', include('chat.urls', namespace='chat')), + path('discussion/',DiscussionPage.as_view(), name='discussion_page') +] + + +urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) diff --git a/Lone_Developers/Lone_Developers/hack72/wsgi.py b/Lone_Developers/Lone_Developers/hack72/wsgi.py new file mode 100644 index 0000000..6cc8ed0 --- /dev/null +++ b/Lone_Developers/Lone_Developers/hack72/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for hack72 project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hack72.settings') + +application = get_wsgi_application() diff --git a/Lone_Developers/Lone_Developers/manage.py b/Lone_Developers/Lone_Developers/manage.py new file mode 100644 index 0000000..c0322e8 --- /dev/null +++ b/Lone_Developers/Lone_Developers/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hack72.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/Lone_Developers/Lone_Developers/media/discussion/180102054_EE692_A04.pdf b/Lone_Developers/Lone_Developers/media/discussion/180102054_EE692_A04.pdf new file mode 100644 index 0000000..9b5ce78 Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/discussion/180102054_EE692_A04.pdf differ diff --git a/Lone_Developers/Lone_Developers/media/discussion/180108057_DETAssignment4.pdf b/Lone_Developers/Lone_Developers/media/discussion/180108057_DETAssignment4.pdf new file mode 100644 index 0000000..42b586d Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/discussion/180108057_DETAssignment4.pdf differ diff --git a/Lone_Developers/Lone_Developers/media/discussion/ALL_INDIA_1000_32_TrainTest_RMSE_Mean_Comparison_3.png b/Lone_Developers/Lone_Developers/media/discussion/ALL_INDIA_1000_32_TrainTest_RMSE_Mean_Comparison_3.png new file mode 100644 index 0000000..e0bd50c Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/discussion/ALL_INDIA_1000_32_TrainTest_RMSE_Mean_Comparison_3.png differ diff --git a/Lone_Developers/Lone_Developers/media/discussion/MH-multi-02.png b/Lone_Developers/Lone_Developers/media/discussion/MH-multi-02.png new file mode 100644 index 0000000..6ad5e76 Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/discussion/MH-multi-02.png differ diff --git a/Lone_Developers/Lone_Developers/media/discussion/Screenshot_from_2020-10-15_23-47-54.png b/Lone_Developers/Lone_Developers/media/discussion/Screenshot_from_2020-10-15_23-47-54.png new file mode 100644 index 0000000..0e4f975 Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/discussion/Screenshot_from_2020-10-15_23-47-54.png differ diff --git a/Lone_Developers/Lone_Developers/media/discussion/back.svg b/Lone_Developers/Lone_Developers/media/discussion/back.svg new file mode 100644 index 0000000..b50ed8a --- /dev/null +++ b/Lone_Developers/Lone_Developers/media/discussion/back.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lone_Developers/Lone_Developers/media/files/180108051_Ass04_DET.pdf b/Lone_Developers/Lone_Developers/media/files/180108051_Ass04_DET.pdf new file mode 100644 index 0000000..125bf4c Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/files/180108051_Ass04_DET.pdf differ diff --git a/Lone_Developers/Lone_Developers/media/files/180108051_Ass04_DET_28T3J52.pdf b/Lone_Developers/Lone_Developers/media/files/180108051_Ass04_DET_28T3J52.pdf new file mode 100644 index 0000000..125bf4c Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/files/180108051_Ass04_DET_28T3J52.pdf differ diff --git a/Lone_Developers/Lone_Developers/media/files/180108051_EE312_TheoryExam_2_AnswerScript.pdf b/Lone_Developers/Lone_Developers/media/files/180108051_EE312_TheoryExam_2_AnswerScript.pdf new file mode 100644 index 0000000..1332566 Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/files/180108051_EE312_TheoryExam_2_AnswerScript.pdf differ diff --git a/Lone_Developers/Lone_Developers/media/images/Delhi-univ-01.png b/Lone_Developers/Lone_Developers/media/images/Delhi-univ-01.png new file mode 100644 index 0000000..fcdb0be Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/images/Delhi-univ-01.png differ diff --git a/Lone_Developers/Lone_Developers/media/images/Delhi-univ-01_HoTOtTH.png b/Lone_Developers/Lone_Developers/media/images/Delhi-univ-01_HoTOtTH.png new file mode 100644 index 0000000..fcdb0be Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/images/Delhi-univ-01_HoTOtTH.png differ diff --git a/Lone_Developers/Lone_Developers/media/images/back.svg b/Lone_Developers/Lone_Developers/media/images/back.svg new file mode 100644 index 0000000..b50ed8a --- /dev/null +++ b/Lone_Developers/Lone_Developers/media/images/back.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Lone_Developers/Lone_Developers/media/images/d1.png b/Lone_Developers/Lone_Developers/media/images/d1.png new file mode 100644 index 0000000..d6810eb Binary files /dev/null and b/Lone_Developers/Lone_Developers/media/images/d1.png differ diff --git a/Lone_Developers/Lone_Developers/parent/.gitignore b/Lone_Developers/Lone_Developers/parent/.gitignore new file mode 100644 index 0000000..2e55d70 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +migrations/ \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/parent/__init__.py b/Lone_Developers/Lone_Developers/parent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/parent/admin.py b/Lone_Developers/Lone_Developers/parent/admin.py new file mode 100644 index 0000000..10812c1 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import * + + +admin.site.register(Holidays) +admin.site.register(Events) +# Register your models here. diff --git a/Lone_Developers/Lone_Developers/parent/apps.py b/Lone_Developers/Lone_Developers/parent/apps.py new file mode 100644 index 0000000..0212c65 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ParentConfig(AppConfig): + name = 'parent' diff --git a/Lone_Developers/Lone_Developers/parent/models.py b/Lone_Developers/Lone_Developers/parent/models.py new file mode 100644 index 0000000..fd733ff --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/models.py @@ -0,0 +1,20 @@ +from django.db import models + +# Create your models here. + +class Events(models.Model) : + title = models.CharField(max_length=100) + description = models.TextField() + date = models.DateTimeField() + + class Meta: + ordering = ["-date"] + +class Holidays(models.Model) : + title = models.CharField(max_length=100) + date = models.DateField() + class Meta: + ordering = ["-date"] + + + diff --git a/Lone_Developers/Lone_Developers/parent/static/css/parent.css b/Lone_Developers/Lone_Developers/parent/static/css/parent.css new file mode 100644 index 0000000..aae5365 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/static/css/parent.css @@ -0,0 +1,49 @@ +.scroll { + max-height: 220px; + overflow-y: auto; +} +.navbar { + margin-bottom: 15px; + font-family: "Source Sans Pro", sans-serif; + font-weight: 700; + padding: 0 10px 0 10px; +} + +.navbar .navbar-brand img { + width: 40px; + height: 40px; +} +.nav-link { + color: #48bb78 !important; +} +body { + background-image: url("/static/images/undraw_happy_news_hxmt.svg"); + background-repeat: no-repeat; + background-size: cover; + background-attachment: fixed; + font-family: "Source Sans Pro", sans-serif; +} +.card-header { + background-color: #ffffff; + color: #48bb78; + font-family: "Source Sans Pro", sans-serif; + font-weight: 700; +} +.card { + margin-left: 29px; +} +.row { + justify-content: space-around; + margin-bottom: 120px; +} +.list-group { + font-family: "Source Sans Pro", sans-serif; + font-weight: 400; +} +#logout { + font-family: "Source Sans Pro", sans-serif; + font-weight: 700; +} +.navbar-toggler { + border: none; +} diff --git a/Lone_Developers/Lone_Developers/parent/static/images/undraw_Graduation_ktn0.svg b/Lone_Developers/Lone_Developers/parent/static/images/undraw_Graduation_ktn0.svg new file mode 100644 index 0000000..8fd6959 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/static/images/undraw_Graduation_ktn0.svg @@ -0,0 +1 @@ +Graduation \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/parent/static/images/undraw_happy_news_hxmt.svg b/Lone_Developers/Lone_Developers/parent/static/images/undraw_happy_news_hxmt.svg new file mode 100644 index 0000000..f2b7161 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/static/images/undraw_happy_news_hxmt.svg @@ -0,0 +1 @@ +happy_news \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/parent/static/images/undraw_news_go0e.svg b/Lone_Developers/Lone_Developers/parent/static/images/undraw_news_go0e.svg new file mode 100644 index 0000000..823f22c --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/static/images/undraw_news_go0e.svg @@ -0,0 +1 @@ +news \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/parent/templates/parent/parent.html b/Lone_Developers/Lone_Developers/parent/templates/parent/parent.html new file mode 100644 index 0000000..618e818 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/templates/parent/parent.html @@ -0,0 +1,161 @@ +{% load static %} + + + + + + parent homepage + + + + + + + + + + + +
    +
    +
    +
    + Announcements +
    +
      + {% for a in announcements %} +
    • +

      Course: {{a.course.title}}

      +

      {{a.content}}

      +

      {{a.created}}

      +
    • + + {% endfor %} +
    +
    +
    + +
    +
    +
    + Events +
    +
      + {% for a in events %} +
    • +

      {{a.title}}

      +

      {{a.description}}

      +

      {{a.date}}

      +
    • + + {% endfor %} + + + +
    +
    +
    +
    +
    +
    + Upcoming Holidays +
    +
      + {% for a in holidays %} +
    • +

      {{a.title}}

      +

      {{a.date}}

      +
    • + {% endfor %} +
    +
    +
    +
    + +
    +
    +
    +

    DISCUSSION FORUM

    +

    Features of our discussion forum

    +
    +
    +
    +
    + + + + + Easy To Use +
    +
    +
    +
    + + + + + Chat with your kid's Instructors +
    +
    +
    +
    + + + + + Share study material relevant to your kids +
    +
    +
    +
    + + + + + Can be used as a PTM online +
    +
    + +
    + +
    +
    + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/parent/templates/parent/report.html b/Lone_Developers/Lone_Developers/parent/templates/parent/report.html new file mode 100644 index 0000000..307736f --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/templates/parent/report.html @@ -0,0 +1,67 @@ + + + + + + parent homepage + + + + + + + + + + + + + +
    +
    +
    +

    {{student.user.username}}

    +

    Roll no : {{student.roll_no}}

    +

    Standard : {{student.standard}}

    +
    + {% for r in reports %} + +
    +
    + +
    +

    {{r.course.title}}

    +

    Teacher : {{r.course.owner.username}}

    +

    Grade : {{r.grade}}

    +

    Attenance : {{r.attendance}}

    + +
    +
    +
    + {% endfor %} + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + diff --git a/Lone_Developers/Lone_Developers/parent/tests.py b/Lone_Developers/Lone_Developers/parent/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Lone_Developers/Lone_Developers/parent/urls.py b/Lone_Developers/Lone_Developers/parent/urls.py new file mode 100644 index 0000000..5ec7d48 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views +app_name="parents" +urlpatterns = [ + path('',views.parent_index,name="parent_index"), + path('gradecard/',views.grade_card,name="grade_card") +] diff --git a/Lone_Developers/Lone_Developers/parent/views.py b/Lone_Developers/Lone_Developers/parent/views.py new file mode 100644 index 0000000..6662156 --- /dev/null +++ b/Lone_Developers/Lone_Developers/parent/views.py @@ -0,0 +1,43 @@ +from django.shortcuts import render +from .models import * +from courses.models import Announcement,Course +from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect +from courses.models import Course +from accounts.models import Student +from datetime import datetime +# Create your views here. + +@login_required(login_url = '/accounts/login/') +def parent_index(request) : + + user = request.user + + if user.role !="P" : + return redirect('accounts:logout') + + courses = Course.objects.filter(student_courses__in= user.parent.children.all()).all() + announcements = Announcement.objects.filter(course__in =courses).all() + + holidays = Holidays.objects.filter(date__gte = datetime.now()).all() + events = Events.objects.filter(date__gte = datetime.now()).all() + context ={ + 'announcements' : announcements, + 'holidays': holidays, + 'events' : Events.objects.filter(date__gte = datetime.now()).all(), + 'childs' : user.parent.children.all(), + } + + return render(request,'parent/parent.html',context) + + + +def grade_card(request, pk) : + student = Student.objects.get(pk=pk) + reports = student.reports.all() + + return render(request,'parent/report.html',{'reports':reports,'student':student}) + + + + diff --git a/Lone_Developers/Lone_Developers/requirements.txt b/Lone_Developers/Lone_Developers/requirements.txt new file mode 100644 index 0000000..dd2de71 --- /dev/null +++ b/Lone_Developers/Lone_Developers/requirements.txt @@ -0,0 +1,44 @@ +aioredis==1.3.1 +asgiref==3.3.0 +async-timeout==3.0.1 +attrs==20.2.0 +autobahn==20.7.1 +Automat==20.2.0 +certifi==2020.6.20 +cffi==1.14.3 +channels==2.4.0 +channels-redis==2.4.2 +chardet==3.0.4 +constantly==15.1.0 +cryptography==3.1.1 +daphne==2.5.0 +Django==3.0.1 +django-braces==1.14.0 +django-channels==0.7.0 +django-embed-video==1.3.3 +django-memcache-status==2.2 +djangorestframework==3.12.1 +hiredis==1.1.0 +hyperlink==20.0.1 +idna==2.10 +incremental==17.5.0 +msgpack==0.6.2 +oauthlib==3.1.0 +Pillow==7.0.0 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycparser==2.20 +PyHamcrest==2.0.2 +pyOpenSSL==19.1.0 +python-memcached==1.59 +pytz==2020.1 +requests==2.24.0 +requests-oauthlib==1.3.0 +service-identity==18.1.0 +six==1.15.0 +sqlparse==0.4.1 +Twisted==20.3.0 +txaio==20.4.1 +urllib3==1.25.10 +whitenoise==5.0.1 +zope.interface==5.1.2 diff --git a/Lone_Developers/Lone_Developers/static/css/chatroom.css b/Lone_Developers/Lone_Developers/static/css/chatroom.css new file mode 100644 index 0000000..d7a4e0a --- /dev/null +++ b/Lone_Developers/Lone_Developers/static/css/chatroom.css @@ -0,0 +1,269 @@ +body{ + + background-image: url('https://images.unsplash.com/photo-1462536943532-57a629f6cc60?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1052&q=80'); +} +.bdy{ + height: 100%; + margin: 0; + background: #ffeef2; + font-family: sans-serif; +} + +.chat{ + margin-top: 0; + margin-bottom: auto; + height:auto ; + min-height: 100vh; + overflow-y: auto; +} +.card{ + width:100% ; + margin-top: 5vh ; + height: 90vh; + min-height: 90vh; + border-radius: 15px !important; + background-color: rgba(0, 0, 0, 0.4); +} +.contacts_body{ + padding: 0.75rem 0 !important; + overflow-y: auto; + white-space: nowrap; + background-color: #baffc793; +} +.msg_card_body{ + background-color: #baffc793; + overflow-y: auto; +} +.card-header{ + + background-color: #3bd75193; + border-radius: 15px 15px 0 0 !important; + border-bottom: 0 !important; +} +.card-footer{ + background-color: #3bd75193; + border-radius: 0 0 15px 15px !important; + border-top: 0 !important; +} +.container{ + align-content: center; +} +.search{ + border-radius: 15px 0 0 15px !important; + background-color: rgba(0,0,0,0.3) !important; + border:0 !important; + color:white !important; +} +.search:focus{ + box-shadow:none !important; + outline:0px !important; +} +.type_msg{ + background-color: rgba(0,0,0,0.3) !important; + border:0 !important; + color:white !important; + height: 50px !important; + overflow-y: auto; +} + .type_msg:focus{ + box-shadow:none !important; + outline:0px !important; +} +.attach_btn{ +border-radius: 15px 0 0 15px !important; +background-color: rgba(0,0,0,0.3) !important; + border:0 !important; + color: white !important; + cursor: pointer; +} +.send_btn{ +border-radius: 0 15px 15px 0 !important; +background-color: rgba(0,0,0,0.3) !important; + border:0 !important; + color: white !important; + cursor: pointer; +} +.search_btn{ + border-radius: 0 15px 15px 0 !important; + background-color: rgba(0,0,0,0.3) !important; + border:0 !important; + color: white !important; + cursor: pointer; +} +.contacts{ + list-style: none; + padding: 0; +} +.contacts li{ + width: 100% !important; + padding: 5px 10px; + margin-bottom: 15px !important; +} +.active{ + background-color: rgba(0,0,0,0.3); +} +.user_img{ + height: 70px; + width: 70px; + border:1.5px solid #f5f6fa; + +} +.user_img_msg{ + height: 40px; + width: 40px; + border:1.5px solid #f5f6fa; + +} +.img_cont{ + position: relative; + height: 70px; + width: 70px; +} +.img_cont_msg{ + height: 40px; + width: 40px; +} +.online_icon{ +position: absolute; +height: 15px; +width:15px; +background-color: #4cd137; +border-radius: 50%; +bottom: 0.2em; +right: 0.4em; +border:1.5px solid white; +} +.offline{ +background-color: #c23616 !important; +} +.user_info{ +margin-top: auto; +margin-bottom: auto; +margin-left: 15px; +} +.user_info span{ +font-size: 20px; +color: white; +} +.user_info p{ +font-size: 10px; +color: rgba(255,255,255,0.6); +} +.video_cam{ +margin-left: 50px; +margin-top: 5px; +} +.video_cam span{ +color: white; +font-size: 20px; +cursor: pointer; +margin-right: 20px; +} +.msg_cotainer{ +margin-top: auto; +margin-bottom: auto; +margin-left: 10px; +border-radius: 5px; +background-color: #82ccdd; +padding: 2px 10px; +position: relative; +color : white ; +font-size : 16px!important; +font-weight: 300 !important; +line-height: 17px; + +} +.msg_cotainer_send{ +margin-top: auto; +margin-bottom: auto; +margin-right: 10px; +border-radius: 5px; +background-color: #78e08f; +padding:2px 10px; +position: relative; +color : white ; +font-size : 16px ; +font-weight: 300; +line-height: 17px; + +} +.msg_time{ +position: absolute; +left: 0; +bottom: -15px; +color: rgba(255,255,255,0.5); +font-size: 10px; +} +.msg_time_send{ +position: absolute; +right:0; +bottom: -15px; +color: rgba(255,255,255,0.5); +font-size: 10px; +} +.msg_head{ +position: relative; +} +#action_menu_btn{ +position: absolute; +right: 10px; +top: 10px; +color: white; +cursor: pointer; +font-size: 20px; +} +.action_menu{ +z-index: 1; +position: absolute; +padding: 15px 0; +background-color: rgba(0,0,0,0.5); +color: white; +border-radius: 15px; +top: 30px; +right: 15px; +display: none; +} +.action_menu ul{ +list-style: none; +padding: 0; +margin: 0; +} +.action_menu ul li{ +width: 100%; +padding: 10px 15px; +margin-bottom: 5px; +} +.action_menu ul li i{ +padding-right: 10px; + +} +.action_menu ul li:hover{ +cursor: pointer; +background-color: rgba(0,0,0,0.2); +} +@media(max-width: 576px){ +.contacts_card{ + /* margin: 40px !important ; */ +margin-bottom: 15px !important; +} +} + +#chat-message-submit{ + width : 5rem ; +} + +*{ + padding:0px; + margin:0px; + box-sizing: border-box; +} + +.typing{ + background-color:#ccab9793 ; +} +.type{ + height:40px ; + padding:0 20px ; + border-radius: 50px; + background-color:#82ccdd ; +} diff --git a/Lone_Developers/Lone_Developers/static/js/reconnecting-websocket.js b/Lone_Developers/Lone_Developers/static/js/reconnecting-websocket.js new file mode 100644 index 0000000..28f6a4b --- /dev/null +++ b/Lone_Developers/Lone_Developers/static/js/reconnecting-websocket.js @@ -0,0 +1,366 @@ + +// MIT License: +// +// Copyright (c) 2010-2012, Joe Walnes +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * This behaves like a WebSocket in every way, except if it fails to connect, + * or it gets disconnected, it will repeatedly poll until it successfully connects + * again. + * + * It is API compatible, so when you have: + * ws = new WebSocket('ws://....'); + * you can replace with: + * ws = new ReconnectingWebSocket('ws://....'); + * + * The event stream will typically look like: + * onconnecting + * onopen + * onmessage + * onmessage + * onclose // lost connection + * onconnecting + * onopen // sometime later... + * onmessage + * onmessage + * etc... + * + * It is API compatible with the standard WebSocket API, apart from the following members: + * + * - `bufferedAmount` + * - `extensions` + * - `binaryType` + * + * Latest version: https://github.com/joewalnes/reconnecting-websocket/ + * - Joe Walnes + * + * Syntax + * ====== + * var socket = new ReconnectingWebSocket(url, protocols, options); + * + * Parameters + * ========== + * url - The url you are connecting to. + * protocols - Optional string or array of protocols. + * options - See below + * + * Options + * ======= + * Options can either be passed upon instantiation or set after instantiation: + * + * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 }); + * + * or + * + * var socket = new ReconnectingWebSocket(url); + * socket.debug = true; + * socket.reconnectInterval = 4000; + * + * debug + * - Whether this instance should log debug messages. Accepts true or false. Default: false. + * + * automaticOpen + * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close(). + * + * reconnectInterval + * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000. + * + * maxReconnectInterval + * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000. + * + * reconnectDecay + * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5. + * + * timeoutInterval + * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000. + * + */ +(function (global, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module !== 'undefined' && module.exports){ + module.exports = factory(); + } else { + global.ReconnectingWebSocket = factory(); + } +})(this, function () { + + if (!('WebSocket' in window)) { + return; + } + + function ReconnectingWebSocket(url, protocols, options) { + + // Default settings + var settings = { + + /** Whether this instance should log debug messages. */ + debug: false, + + /** Whether or not the websocket should attempt to connect immediately upon instantiation. */ + automaticOpen: true, + + /** The number of milliseconds to delay before attempting to reconnect. */ + reconnectInterval: 1000, + /** The maximum number of milliseconds to delay a reconnection attempt. */ + maxReconnectInterval: 30000, + /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */ + reconnectDecay: 1.5, + + /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */ + timeoutInterval: 2000, + + /** The maximum number of reconnection attempts to make. Unlimited if null. */ + maxReconnectAttempts: null, + + /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */ + binaryType: 'blob' + } + if (!options) { options = {}; } + + // Overwrite and define settings with options if they exist. + for (var key in settings) { + if (typeof options[key] !== 'undefined') { + this[key] = options[key]; + } else { + this[key] = settings[key]; + } + } + + // These should be treated as read-only properties + + /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */ + this.url = url; + + /** The number of attempted reconnects since starting, or the last successful connection. Read only. */ + this.reconnectAttempts = 0; + + /** + * The current state of the connection. + * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED + * Read only. + */ + this.readyState = WebSocket.CONNECTING; + + /** + * A string indicating the name of the sub-protocol the server selected; this will be one of + * the strings specified in the protocols parameter when creating the WebSocket object. + * Read only. + */ + this.protocol = null; + + // Private state variables + + var self = this; + var ws; + var forcedClose = false; + var timedOut = false; + var eventTarget = document.createElement('div'); + + // Wire up "on*" properties as event handlers + + eventTarget.addEventListener('open', function(event) { self.onopen(event); }); + eventTarget.addEventListener('close', function(event) { self.onclose(event); }); + eventTarget.addEventListener('connecting', function(event) { self.onconnecting(event); }); + eventTarget.addEventListener('message', function(event) { self.onmessage(event); }); + eventTarget.addEventListener('error', function(event) { self.onerror(event); }); + + // Expose the API required by EventTarget + + this.addEventListener = eventTarget.addEventListener.bind(eventTarget); + this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); + this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); + + /** + * This function generates an event that is compatible with standard + * compliant browsers and IE9 - IE11 + * + * This will prevent the error: + * Object doesn't support this action + * + * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563 + * @param s String The name that the event should use + * @param args Object an optional object that the event will use + */ + function generateEvent(s, args) { + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(s, false, false, args); + return evt; + }; + + this.open = function (reconnectAttempt) { + ws = new WebSocket(self.url, protocols || []); + ws.binaryType = this.binaryType; + + if (reconnectAttempt) { + if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) { + return; + } + } else { + eventTarget.dispatchEvent(generateEvent('connecting')); + this.reconnectAttempts = 0; + } + + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'attempt-connect', self.url); + } + + var localWs = ws; + var timeout = setTimeout(function() { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'connection-timeout', self.url); + } + timedOut = true; + localWs.close(); + timedOut = false; + }, self.timeoutInterval); + + ws.onopen = function(event) { + clearTimeout(timeout); + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onopen', self.url); + } + self.protocol = ws.protocol; + self.readyState = WebSocket.OPEN; + self.reconnectAttempts = 0; + var e = generateEvent('open'); + e.isReconnect = reconnectAttempt; + reconnectAttempt = false; + eventTarget.dispatchEvent(e); + }; + + ws.onclose = function(event) { + clearTimeout(timeout); + ws = null; + if (forcedClose) { + self.readyState = WebSocket.CLOSED; + eventTarget.dispatchEvent(generateEvent('close')); + } else { + self.readyState = WebSocket.CONNECTING; + var e = generateEvent('connecting'); + e.code = event.code; + e.reason = event.reason; + e.wasClean = event.wasClean; + eventTarget.dispatchEvent(e); + if (!reconnectAttempt && !timedOut) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onclose', self.url); + } + eventTarget.dispatchEvent(generateEvent('close')); + } + + var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts); + setTimeout(function() { + self.reconnectAttempts++; + self.open(true); + }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout); + } + }; + ws.onmessage = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data); + } + var e = generateEvent('message'); + e.data = event.data; + eventTarget.dispatchEvent(e); + }; + ws.onerror = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onerror', self.url, event); + } + eventTarget.dispatchEvent(generateEvent('error')); + }; + } + + // Whether or not to create a websocket upon instantiation + if (this.automaticOpen == true) { + this.open(false); + } + + /** + * Transmits data to the server over the WebSocket connection. + * + * @param data a text string, ArrayBuffer or Blob to send to the server. + */ + this.send = function(data) { + if (ws) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'send', self.url, data); + } + return ws.send(data); + } else { + throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; + } + }; + + /** + * Closes the WebSocket connection or connection attempt, if any. + * If the connection is already CLOSED, this method does nothing. + */ + this.close = function(code, reason) { + // Default CLOSE_NORMAL code + if (typeof code == 'undefined') { + code = 1000; + } + forcedClose = true; + if (ws) { + ws.close(code, reason); + } + }; + + /** + * Additional public API method to refresh the connection if still open (close, re-open). + * For example, if the app suspects bad data / missed heart beats, it can try to refresh. + */ + this.refresh = function() { + if (ws) { + ws.close(); + } + }; + } + + /** + * An event listener to be called when the WebSocket connection's readyState changes to OPEN; + * this indicates that the connection is ready to send and receive data. + */ + ReconnectingWebSocket.prototype.onopen = function(event) {}; + /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */ + ReconnectingWebSocket.prototype.onclose = function(event) {}; + /** An event listener to be called when a connection begins being attempted. */ + ReconnectingWebSocket.prototype.onconnecting = function(event) {}; + /** An event listener to be called when a message is received from the server. */ + ReconnectingWebSocket.prototype.onmessage = function(event) {}; + /** An event listener to be called when an error occurs. */ + ReconnectingWebSocket.prototype.onerror = function(event) {}; + + /** + * Whether all instances of ReconnectingWebSocket should log debug messages. + * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. + */ + ReconnectingWebSocket.debugAll = false; + + ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING; + ReconnectingWebSocket.OPEN = WebSocket.OPEN; + ReconnectingWebSocket.CLOSING = WebSocket.CLOSING; + ReconnectingWebSocket.CLOSED = WebSocket.CLOSED; + + return ReconnectingWebSocket; +}); diff --git a/Lone_Developers/Lone_Developers/static/js/reconnecting_websocket.js b/Lone_Developers/Lone_Developers/static/js/reconnecting_websocket.js new file mode 100644 index 0000000..28f6a4b --- /dev/null +++ b/Lone_Developers/Lone_Developers/static/js/reconnecting_websocket.js @@ -0,0 +1,366 @@ + +// MIT License: +// +// Copyright (c) 2010-2012, Joe Walnes +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * This behaves like a WebSocket in every way, except if it fails to connect, + * or it gets disconnected, it will repeatedly poll until it successfully connects + * again. + * + * It is API compatible, so when you have: + * ws = new WebSocket('ws://....'); + * you can replace with: + * ws = new ReconnectingWebSocket('ws://....'); + * + * The event stream will typically look like: + * onconnecting + * onopen + * onmessage + * onmessage + * onclose // lost connection + * onconnecting + * onopen // sometime later... + * onmessage + * onmessage + * etc... + * + * It is API compatible with the standard WebSocket API, apart from the following members: + * + * - `bufferedAmount` + * - `extensions` + * - `binaryType` + * + * Latest version: https://github.com/joewalnes/reconnecting-websocket/ + * - Joe Walnes + * + * Syntax + * ====== + * var socket = new ReconnectingWebSocket(url, protocols, options); + * + * Parameters + * ========== + * url - The url you are connecting to. + * protocols - Optional string or array of protocols. + * options - See below + * + * Options + * ======= + * Options can either be passed upon instantiation or set after instantiation: + * + * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 }); + * + * or + * + * var socket = new ReconnectingWebSocket(url); + * socket.debug = true; + * socket.reconnectInterval = 4000; + * + * debug + * - Whether this instance should log debug messages. Accepts true or false. Default: false. + * + * automaticOpen + * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close(). + * + * reconnectInterval + * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000. + * + * maxReconnectInterval + * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000. + * + * reconnectDecay + * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5. + * + * timeoutInterval + * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000. + * + */ +(function (global, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module !== 'undefined' && module.exports){ + module.exports = factory(); + } else { + global.ReconnectingWebSocket = factory(); + } +})(this, function () { + + if (!('WebSocket' in window)) { + return; + } + + function ReconnectingWebSocket(url, protocols, options) { + + // Default settings + var settings = { + + /** Whether this instance should log debug messages. */ + debug: false, + + /** Whether or not the websocket should attempt to connect immediately upon instantiation. */ + automaticOpen: true, + + /** The number of milliseconds to delay before attempting to reconnect. */ + reconnectInterval: 1000, + /** The maximum number of milliseconds to delay a reconnection attempt. */ + maxReconnectInterval: 30000, + /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */ + reconnectDecay: 1.5, + + /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */ + timeoutInterval: 2000, + + /** The maximum number of reconnection attempts to make. Unlimited if null. */ + maxReconnectAttempts: null, + + /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */ + binaryType: 'blob' + } + if (!options) { options = {}; } + + // Overwrite and define settings with options if they exist. + for (var key in settings) { + if (typeof options[key] !== 'undefined') { + this[key] = options[key]; + } else { + this[key] = settings[key]; + } + } + + // These should be treated as read-only properties + + /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */ + this.url = url; + + /** The number of attempted reconnects since starting, or the last successful connection. Read only. */ + this.reconnectAttempts = 0; + + /** + * The current state of the connection. + * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED + * Read only. + */ + this.readyState = WebSocket.CONNECTING; + + /** + * A string indicating the name of the sub-protocol the server selected; this will be one of + * the strings specified in the protocols parameter when creating the WebSocket object. + * Read only. + */ + this.protocol = null; + + // Private state variables + + var self = this; + var ws; + var forcedClose = false; + var timedOut = false; + var eventTarget = document.createElement('div'); + + // Wire up "on*" properties as event handlers + + eventTarget.addEventListener('open', function(event) { self.onopen(event); }); + eventTarget.addEventListener('close', function(event) { self.onclose(event); }); + eventTarget.addEventListener('connecting', function(event) { self.onconnecting(event); }); + eventTarget.addEventListener('message', function(event) { self.onmessage(event); }); + eventTarget.addEventListener('error', function(event) { self.onerror(event); }); + + // Expose the API required by EventTarget + + this.addEventListener = eventTarget.addEventListener.bind(eventTarget); + this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); + this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); + + /** + * This function generates an event that is compatible with standard + * compliant browsers and IE9 - IE11 + * + * This will prevent the error: + * Object doesn't support this action + * + * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563 + * @param s String The name that the event should use + * @param args Object an optional object that the event will use + */ + function generateEvent(s, args) { + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(s, false, false, args); + return evt; + }; + + this.open = function (reconnectAttempt) { + ws = new WebSocket(self.url, protocols || []); + ws.binaryType = this.binaryType; + + if (reconnectAttempt) { + if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) { + return; + } + } else { + eventTarget.dispatchEvent(generateEvent('connecting')); + this.reconnectAttempts = 0; + } + + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'attempt-connect', self.url); + } + + var localWs = ws; + var timeout = setTimeout(function() { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'connection-timeout', self.url); + } + timedOut = true; + localWs.close(); + timedOut = false; + }, self.timeoutInterval); + + ws.onopen = function(event) { + clearTimeout(timeout); + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onopen', self.url); + } + self.protocol = ws.protocol; + self.readyState = WebSocket.OPEN; + self.reconnectAttempts = 0; + var e = generateEvent('open'); + e.isReconnect = reconnectAttempt; + reconnectAttempt = false; + eventTarget.dispatchEvent(e); + }; + + ws.onclose = function(event) { + clearTimeout(timeout); + ws = null; + if (forcedClose) { + self.readyState = WebSocket.CLOSED; + eventTarget.dispatchEvent(generateEvent('close')); + } else { + self.readyState = WebSocket.CONNECTING; + var e = generateEvent('connecting'); + e.code = event.code; + e.reason = event.reason; + e.wasClean = event.wasClean; + eventTarget.dispatchEvent(e); + if (!reconnectAttempt && !timedOut) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onclose', self.url); + } + eventTarget.dispatchEvent(generateEvent('close')); + } + + var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts); + setTimeout(function() { + self.reconnectAttempts++; + self.open(true); + }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout); + } + }; + ws.onmessage = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data); + } + var e = generateEvent('message'); + e.data = event.data; + eventTarget.dispatchEvent(e); + }; + ws.onerror = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onerror', self.url, event); + } + eventTarget.dispatchEvent(generateEvent('error')); + }; + } + + // Whether or not to create a websocket upon instantiation + if (this.automaticOpen == true) { + this.open(false); + } + + /** + * Transmits data to the server over the WebSocket connection. + * + * @param data a text string, ArrayBuffer or Blob to send to the server. + */ + this.send = function(data) { + if (ws) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'send', self.url, data); + } + return ws.send(data); + } else { + throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; + } + }; + + /** + * Closes the WebSocket connection or connection attempt, if any. + * If the connection is already CLOSED, this method does nothing. + */ + this.close = function(code, reason) { + // Default CLOSE_NORMAL code + if (typeof code == 'undefined') { + code = 1000; + } + forcedClose = true; + if (ws) { + ws.close(code, reason); + } + }; + + /** + * Additional public API method to refresh the connection if still open (close, re-open). + * For example, if the app suspects bad data / missed heart beats, it can try to refresh. + */ + this.refresh = function() { + if (ws) { + ws.close(); + } + }; + } + + /** + * An event listener to be called when the WebSocket connection's readyState changes to OPEN; + * this indicates that the connection is ready to send and receive data. + */ + ReconnectingWebSocket.prototype.onopen = function(event) {}; + /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */ + ReconnectingWebSocket.prototype.onclose = function(event) {}; + /** An event listener to be called when a connection begins being attempted. */ + ReconnectingWebSocket.prototype.onconnecting = function(event) {}; + /** An event listener to be called when a message is received from the server. */ + ReconnectingWebSocket.prototype.onmessage = function(event) {}; + /** An event listener to be called when an error occurs. */ + ReconnectingWebSocket.prototype.onerror = function(event) {}; + + /** + * Whether all instances of ReconnectingWebSocket should log debug messages. + * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. + */ + ReconnectingWebSocket.debugAll = false; + + ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING; + ReconnectingWebSocket.OPEN = WebSocket.OPEN; + ReconnectingWebSocket.CLOSING = WebSocket.CLOSING; + ReconnectingWebSocket.CLOSED = WebSocket.CLOSED; + + return ReconnectingWebSocket; +}); diff --git a/Lone_Developers/Lone_Developers/students/.gitignore b/Lone_Developers/Lone_Developers/students/.gitignore new file mode 100644 index 0000000..2e55d70 --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +migrations/ \ No newline at end of file diff --git a/Lone_Developers/Lone_Developers/students/__init__.py b/Lone_Developers/Lone_Developers/students/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Lone_Developers/Lone_Developers/students/admin.py b/Lone_Developers/Lone_Developers/students/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/Lone_Developers/Lone_Developers/students/apps.py b/Lone_Developers/Lone_Developers/students/apps.py new file mode 100644 index 0000000..069ab89 --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class StudentsConfig(AppConfig): + name = 'students' diff --git a/Lone_Developers/Lone_Developers/students/forms.py b/Lone_Developers/Lone_Developers/students/forms.py new file mode 100644 index 0000000..f36ff15 --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/forms.py @@ -0,0 +1,7 @@ +from django import forms +from courses.models import Course + + +class CourseEnrollForm(forms.Form): + course = forms.ModelChoiceField(queryset=Course.objects.all(), + widget=forms.HiddenInput) diff --git a/Lone_Developers/Lone_Developers/students/models.py b/Lone_Developers/Lone_Developers/students/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/Lone_Developers/Lone_Developers/students/templates/students/course/all_courses.html b/Lone_Developers/Lone_Developers/students/templates/students/course/all_courses.html new file mode 100644 index 0000000..413aed4 --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/templates/students/course/all_courses.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %}My courses{% endblock %} + +{% block content %} + {% comment %}

    My courses

    {% endcomment %} +
    +
    + +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/students/templates/students/course/detail.html b/Lone_Developers/Lone_Developers/students/templates/students/course/detail.html new file mode 100644 index 0000000..ac1cf3b --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/templates/students/course/detail.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} +{% load cache %} + +{% block title %} + {{ object.title }} +{% endblock %} + +{% block content %} +

    + Go Back +

    +
    +
    +

    Modules for {{course.title}}

    + +

    + + Course chat room + +

    +
    +
    + {% cache 600 module_contents module %} + {% for content in module.contents.all %} + {% with item=content.item %} +

    {{ item.title }}

    + {{ item.render }} + {% endwith %} + {% endfor %} + {% endcache %} + + + + +
    +
    + +

    Announcements

    +
    + {% for a in announcements %} +
    +

    {{a.content}}

    +

    - {{ a.created }}

    +
    +
    + {% endfor %} +
    +
    + + +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/students/templates/students/course/list.html b/Lone_Developers/Lone_Developers/students/templates/students/course/list.html new file mode 100644 index 0000000..aaa8977 --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/templates/students/course/list.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block title %}My courses{% endblock %} + +{% block content %} + {% comment %}

    My courses

    {% endcomment %} +
    +
    +
    +

    You are studing in Standard {{request.user.student.standard}}

    +

    Hi {{request.user}}, Your Enrolled Courses are listed Below!

    +

    Follow your dreams, believe in yourself and don’t give up. – Rachel Corrie

    +
    + +
    + {% for course in object_list %} +
    +

    {{course.title}}

    +

    {{course.overview}}

    +

    Subject: {{course.subject.title}}

    +

    Teacher: {{course.owner.username}}

    + Continue Studying + + + + +
    + {% empty %} +

    + You are not enrolled in any courses yet. + Browse courses + + + + + to enroll in a course. +

    + {% endfor %} +
    + +
    +
    + +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/students/templates/students/student/registration.html b/Lone_Developers/Lone_Developers/students/templates/students/student/registration.html new file mode 100644 index 0000000..39e537f --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/templates/students/student/registration.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %} + Sign up +{% endblock %} + +{% block content %} +

    + Sign up +

    +
    +

    Enter your details to create an account:

    +
    + {{ form.as_p }} + {% csrf_token %} +

    +
    +
    +{% endblock %} diff --git a/Lone_Developers/Lone_Developers/students/tests.py b/Lone_Developers/Lone_Developers/students/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Lone_Developers/Lone_Developers/students/urls.py b/Lone_Developers/Lone_Developers/students/urls.py new file mode 100644 index 0000000..4c66b66 --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/urls.py @@ -0,0 +1,25 @@ +from django.urls import path +from django.views.decorators.cache import cache_page +from . import views +app_name="students" +urlpatterns = [ + path('register/', + views.StudentRegistrationView.as_view(), + name='student_registration'), + path('course_list/', + views.CourseListView.as_view(), + name='course_list'), + + path('enroll-course/', + views.enroll_course, + name='student_enroll_course'), + path('courses/', + views.StudentCourseListView.as_view(), + name='student_course_list'), + path('course//', + cache_page(60 * 15)(views.StudentCourseDetailView.as_view()), + name='student_course_detail'), + path('course///', + cache_page(60 * 15)(views.StudentCourseDetailView.as_view()), + name='student_course_detail_module'), +] diff --git a/Lone_Developers/Lone_Developers/students/views.py b/Lone_Developers/Lone_Developers/students/views.py new file mode 100644 index 0000000..29b452d --- /dev/null +++ b/Lone_Developers/Lone_Developers/students/views.py @@ -0,0 +1,87 @@ +from django.urls import reverse_lazy +from django.shortcuts import redirect +from django.views.generic.edit import CreateView, FormView +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth import authenticate, login +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic.list import ListView +from django.views.generic.detail import DetailView +from courses.models import Course +from .forms import CourseEnrollForm + + +class StudentRegistrationView(CreateView): + template_name = 'students/student/registration.html' + form_class = UserCreationForm + success_url = reverse_lazy('student_course_list') + + def form_valid(self, form): + result = super().form_valid(form) + cd = form.cleaned_data + user = authenticate(username=cd['username'], + password=cd['password1']) + login(self.request, user) + return result + + +def enroll_course(request,pk) : + + course = Course.objects.get(pk=pk) + course.student_courses.add(request.user.student) + chat = course.general_chat + chat.participants.add(request.user) + + return redirect('students:student_course_detail', + pk=course.id) + + + + + + +class CourseListView(ListView): + model = Course + template_name = 'students/course/all_courses.html' + + def get_queryset(self): + qs = super().get_queryset() + if not self.request.user.is_authenticated: + return qs + if self.request.user.role == 'S': + return qs.exclude(student_courses__in=[self.request.user.student]) + else: + return qs + + +class StudentCourseListView(LoginRequiredMixin, ListView): + model = Course + template_name = 'students/course/list.html' + + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(student_courses__in=[self.request.user.student]) + + +class StudentCourseDetailView(DetailView): + model = Course + template_name = 'students/course/detail.html' + + def get_queryset(self): + qs = super().get_queryset() + return qs.filter(student_courses__in=[self.request.user.student]) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # get course object + course = self.get_object() + if 'module_id' in self.kwargs: + # get current module + context['module'] = course.modules.get( + id=self.kwargs['module_id']) + else: + # get first module + context['module'] = course.modules.all()[0] + + context['announcements'] = self.object.announcements.all() + + return context diff --git a/Lone_Developers/README.md b/Lone_Developers/README.md new file mode 100644 index 0000000..83a1358 --- /dev/null +++ b/Lone_Developers/README.md @@ -0,0 +1,84 @@ +# Code-Innovation-Series-IITG + +An Online education platform +Live at https://learn-live.herokuapp.com/ + +## Test User Credentials + +Login -> http://learn-live.herokuapp.com/accounts/login/ + +### Student Login +Username : student01 +Password : common101 + +### Teacher Login +Username : teacher01 +Password : common101 + +### Parent Login +Username : parent01 +Password : common101 + +### Django SuperUser +login -> http://learn-live.herokuapp.com/admin +Username : kunal +Password : 1234 + +## Installation + +* `git clone ` +* Install Dependencies via `pip install -r 'requirements.txt'` +* `python manage.py makemigrations` +* `python manage.py migrate` +* `python manage.py runserver` + + +Note 01: Once user register on portal, admin should login [https://learn-live.herokuapp.com/admin/accounts/user/] and approve the account of that user. Only approved users can view our course content and participate in discussion forum. + +Note 02: All the Test Credentials are approved Users. So you can use them freely! + +## Features + +### Students + +- login and register +- enroll to available courses +- see all courses +- navigation to course where they can see different modules, contents of modules, announcements regarding the course. +- Chat with other students enrolled in that course through course chat room +- Participate in discussion forum with other teachers/parents/students + + +### Teachers +- Login and register +- Create multiple courses +- Edit/create new modules in each course +- Reorder modules whenever you feel necessary! +- Post course content in any way. It can be text/image/video/audio/ppt/pdf/etc... +- Participate in discussion forum with other teachers/parents/students +- Create Announcements in each course separately (Course Specific) + + +### Parents +- Login and register +- See announcements regarding their children's courses that are made by teachers +- Get list of all Events +- Get list of all Holidays +- Check progress of each student in every course (grades,attendance) +- Participate in discussion forum with other teachers/parents/students + +Note : Attendance and grades need to be registered by the admin once finalized. + + +## Technology Used +- Django +- Django Rest Framework +- Django Channels +- Tailblocks +- Javascript +- JQuery +- Html +- CSS + +We tried to follow the best coding principles while participating in this Code-Innovation-Series! +