├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── classroom_manager
├── __init__.py
├── forms.py
├── models.py
├── network.py
├── routes.py
├── site.sqlite3
├── static
│ ├── css
│ │ ├── home.css
│ │ └── styles.css
│ ├── imgs
│ │ ├── 1_vYtDyAbOMTJnArJvXix5wg.jpeg
│ │ ├── 7bbcc1707ce09218fa84866a65b3c871.png
│ │ ├── 800px_COLOURBOX4159486.jpg
│ │ ├── Periodic_Table.png
│ │ ├── call-icon.png
│ │ ├── chat-icon.png
│ │ ├── default_class.png
│ │ ├── defaultuser-icon.png
│ │ ├── dropdown-icon.png
│ │ ├── gear-icon.png
│ │ ├── group-icon.png
│ │ ├── meetings-icon.png
│ │ ├── notification-icon.png
│ │ ├── plus-icon.png
│ │ ├── storage-nand_nor_gates-f_mobile.png
│ │ ├── trig.gif
│ │ └── website_icon.png
│ ├── js
│ │ ├── app.js
│ │ ├── chat.js
│ │ └── classroom.js
│ └── submissions
│ │ └── homeworktest.txt
├── templates
│ ├── activity.html
│ ├── app.html
│ ├── app_layout.html
│ ├── calls.html
│ ├── chats.html
│ ├── classrooms.html
│ ├── home.html
│ ├── layout.html
│ ├── login.html
│ ├── meetings.html
│ └── register.html
└── utils.py
├── requirements.txt
├── run.py
└── screenshots
├── screenshot1.PNG
├── screenshot2.PNG
├── screenshot3.PNG
├── screenshot4.PNG
├── screenshot5.PNG
└── screenshot6.PNG
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # celery beat schedule file
95 | celerybeat-schedule
96 |
97 | # SageMath parsed files
98 | *.sage.py
99 |
100 | # Environments
101 | .env
102 | .venv
103 | env/
104 | venv/
105 | ENV/
106 | env.bak/
107 | venv.bak/
108 |
109 | # Spyder project settings
110 | .spyderproject
111 | .spyproject
112 |
113 | # Rope project settings
114 | .ropeproject
115 |
116 | # mkdocs documentation
117 | /site
118 |
119 | # mypy
120 | .mypy_cache/
121 | .dmypy.json
122 | dmypy.json
123 |
124 | # Pyre type checker
125 | .pyre/
126 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 adri711
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
12 |
13 |
14 | # Classroom Manager
15 |
16 | ## Description
17 | Hey there! This is a website called Classroom Manager. You can create classrooms, have fellow students join the classrooms, chat with your peers, create notes, assign assignments, and more! You can dm 'Top Of Tech#4867' or 'adri711#7835' on discord if you have any thoughts.
18 |
19 | Created by [Top Of Tech](http:www.github.com/Top-Of-Tech) and [adri711](http:www.github.com/adri711)
20 |
21 |
22 | ## Instructions
23 |
24 | NOTE: Make sure to run all commands in the same directory as the files!
25 |
26 | 1. Install requirements
27 | Run this command in your terminal or command prompt: `pip install -r requirements.txt`
28 |
29 | 2. Run the website
30 | Run this command: `python run.py`
31 |
32 | 3. Go to the website
33 | Go to this url: [http://127.0.0.7:5000](http://127.0.0.7:5000)
34 |
35 | 4. Register
36 |
37 | 5. Sign in with your email and password
38 |
39 | 6. Explore the app!
40 |
--------------------------------------------------------------------------------
/classroom_manager/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, url_for
2 | from flask_sqlalchemy import SQLAlchemy
3 | from flask_bcrypt import Bcrypt
4 | from flask_login import LoginManager
5 | from flask_socketio import SocketIO
6 | from os.path import join, dirname, realpath
7 |
8 | app = Flask(__name__)
9 | app.config['SECRET_KEY'] = 'R\x1a^-\x14?mq\x1ce\xf7\xefi\xb7\x0e\xa0\x02\x0c\xd6-$\x033\xc4'
10 | app.config['IMAGE_UPLOADS'] = join(dirname(realpath(__file__)), 'static/imgs/')
11 | app.config['FILE_UPLOADS'] = join(dirname(realpath(__file__)), 'static/submissions/')
12 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
13 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.sqlite3'
14 | db = SQLAlchemy(app)
15 | socketio = SocketIO(app, cors_allowed_origins="*")
16 |
17 | bcrypt = Bcrypt(app)
18 | login_manager = LoginManager(app)
19 | login_manager.login_view = 'login'
20 |
21 | #modules import
22 | from classroom_manager.routes import *
23 | from classroom_manager.models import User
24 | from classroom_manager.network import *
25 | #db.create_all()
26 |
--------------------------------------------------------------------------------
/classroom_manager/forms.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import FlaskForm
2 | from wtforms import StringField, PasswordField, SubmitField, BooleanField, RadioField
3 | from wtforms.validators import DataRequired, Length, Email, EqualTo, InputRequired, ValidationError
4 | from classroom_manager.models import User
5 |
6 | class RegistrationForm(FlaskForm): # Form objects for the front-end devs
7 | name = StringField("Name",
8 | validators=[DataRequired(), Length(min=2, max=25)])
9 | last_name = StringField("Last Name",
10 | validators=[DataRequired(), Length(min=2, max=25)])
11 | username = StringField("Username",
12 | validators=[DataRequired(), Length(min=2, max=25)])
13 | email = StringField("Email",
14 | validators=[DataRequired(), Email()])
15 | password = PasswordField("Password",
16 | validators=[DataRequired()])
17 | confirm_password = PasswordField("Confirm Password",
18 | validators=[DataRequired(), EqualTo('password')])
19 | student_or_teacher = RadioField("Teacher or student",
20 | choices=[('teacher', 'Teacher'), ('student', 'Student')],
21 | validators=[InputRequired()])
22 | submit = SubmitField('Sign Up')
23 |
24 | def validate_username(self, username):
25 | if User.query.filter_by(username=username.data).first(): # Checks if a user with the same username is in the database
26 | raise ValidationError("That username is taken, please choose a different one.")
27 |
28 | def validate_email(self, email):
29 | if User.query.filter_by(email=email.data).first(): # Checks if a user with the same username is in the database
30 | raise ValidationError("That email is taken, please choose a different one.")
31 |
32 | class LoginForm(FlaskForm): # Form objects for the front-end devs
33 | email = StringField("Email",
34 | validators=[DataRequired(), Email()])
35 | password = PasswordField("Password",
36 | validators=[DataRequired()])
37 | remember = BooleanField("Remember Me")
38 | submit = SubmitField('Log In')
39 |
--------------------------------------------------------------------------------
/classroom_manager/models.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 | from classroom_manager import login_manager, db
3 | from flask_login import UserMixin
4 | from datetime import datetime
5 |
6 | @login_manager.user_loader
7 | def load_user(user_id):
8 | return User.query.get(int(user_id))
9 |
10 | class User(db.Model, UserMixin):
11 | id = db.Column(db.Integer, primary_key=True)
12 | first_name = db.Column(db.String(25), nullable=False)
13 | last_name = db.Column(db.String(25), nullable=False)
14 | username = db.Column(db.String(20), unique=True, nullable=False)
15 | email = db.Column(db.String(254), unique=True, nullable=False)
16 | image_file = db.Column(db.String(20), nullable=False, default="default.jpg")
17 | status = db.Column(db.String(10), nullable=False)
18 | password = db.Column(db.String(100), nullable=False)
19 | notes = db.relationship('Note', backref='author', lazy=True)
20 | assignments = db.relationship('Assignment', backref='author', lazy=True)
21 | messages = db.relationship('Message', backref='author', lazy=True)
22 | membership = db.relationship('Membership', backref='classroom', lazy=True)
23 | submission = db.relationship('AssignmentSubmission', backref='submission', lazy=True)
24 | def __repr__(self):
25 | return f"User(username: '{self.username}', email: '{self.email}', profile_img: '{self.image_file}')"
26 |
27 |
28 | class Classroom(db.Model):
29 | id = db.Column(db.Integer, primary_key=True)
30 | code = db.Column(db.String(60), nullable=True)
31 | image_file = db.Column(db.String(42), nullable=False, default="default_class.png")
32 | name = db.Column(db.String(25), nullable=False)
33 | description = db.Column(db.String(262), nullable=True)
34 | membership = db.relationship('Membership', backref='membership', lazy=True)
35 |
36 | def __repr__(self):
37 | return f"Classroom(name: '{self.name}', class_img: '{self.image_file}'"
38 |
39 | class Membership(db.Model):
40 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
41 | classroom_id = db.Column(db.Integer, db.ForeignKey('classroom.id'), nullable=False)
42 | role = db.Column(db.String(7), nullable=False)
43 |
44 | __table_args__ = (
45 | db.PrimaryKeyConstraint(user_id, classroom_id), # Allows two primary keys
46 | )
47 |
48 | def __repr__(self):
49 | return f"Membership(user_id: '{self.user_id}', classroom_id: '{self.classroom_id}')"
50 |
51 | class Message(db.Model):
52 | id = db.Column(db.Integer, primary_key=True)
53 | author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
54 | channel_id = db.Column(db.Integer, db.ForeignKey('channel.id'), nullable=False)
55 | contents = db.Column(db.String(100), nullable=False)
56 | ischild = db.Column(db.Integer, default=-1, nullable=False) # if this is set to -1 then this message is not a reply
57 | date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
58 |
59 | def __repr__(self):
60 | return f"Message(author_id: '{self.author_id}', classroom: '{self.classroom}')"
61 |
62 | class Note(db.Model):
63 | id = db.Column(db.Integer, primary_key=True)
64 | author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
65 | channel_id = db.Column(db.Integer, db.ForeignKey('channel.id'), nullable=False)
66 | title = db.Column(db.String(32), nullable=False)
67 | note_text = db.Column(db.String(300), nullable=False)
68 | note_imgs = db.Column(db.String(52), nullable=True)
69 |
70 | def __repr__(self):
71 | return f"Note(author_id: '{self.author_id}', classroom: '{self.classroom}')"
72 |
73 | class Assignment(db.Model):
74 | id = db.Column(db.Integer, primary_key=True)
75 | author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
76 | channel_id = db.Column(db.Integer, db.ForeignKey('channel.id'), nullable=False)
77 | assignment_text = db.Column(db.String(200), nullable=False)
78 | due_date = db.Column(db.DateTime, nullable=False)
79 | submission = db.relationship('AssignmentSubmission', backref='assignment', lazy=True)
80 | def __repr__(self):
81 | return f"Assignment(author_id: '{self.author_id}', classroom: '{self.classroom}')"
82 |
83 |
84 | class Channel(db.Model):
85 | id = db.Column(db.Integer, primary_key=True)
86 | classroom_id = db.Column(db.Integer, db.ForeignKey('classroom.id'), nullable=False)
87 | name = db.Column(db.String(30), nullable=False)
88 | message = db.relationship('Message', backref='mesage', lazy=True)
89 | note = db.relationship('Note', backref='note', lazy=True)
90 | assignment = db.relationship('Assignment', backref='assignment', lazy=True)
91 |
92 | def __repr__(self):
93 | return f"Channel(classroom: '{self.classroom_id}', name: '{self.name}')"
94 |
95 | class Ban(db.Model): #hasnt been fully implemented yet
96 | id = db.Column(db.Integer, primary_key=True)
97 | classroom_id = db.Column(db.Integer, db.ForeignKey('classroom.id'), nullable=False)
98 | def __repr__(self):
99 | return f"Ban(classroom: '{self.classroom_id}', user_id: '{self.user_id}')"
100 |
101 | class DirectMessage(db.Model):
102 | id = db.Column(db.Integer, primary_key=True)
103 | sender_id = db.Column(db.Integer, nullable=False)
104 | receiver_id = db.Column(db.Integer, nullable=False)
105 | content = db.Column(db.String(100), nullable=False)
106 | date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
107 |
108 | def __repr__(self):
109 | return f"Message(author_id: '{self.sender_id}')"
110 |
111 |
112 | class AssignmentSubmission(db.Model):
113 | id = db.Column(db.Integer, primary_key=True)
114 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
115 | assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id'), nullable=False)
116 | file_location = db.Column(db.String(52), nullable=True)
117 |
118 | def __repr__(self):
119 | return f'submission({id})'
--------------------------------------------------------------------------------
/classroom_manager/network.py:
--------------------------------------------------------------------------------
1 | from flask import session, redirect, url_for,request
2 | from flask_login import current_user
3 | from flask_socketio import SocketIO, emit, send, disconnect, join_room, leave_room
4 | from classroom_manager import socketio
5 | from classroom_manager.models import User, Classroom, Membership, Channel, Message, Note, DirectMessage
6 | from classroom_manager.utils import generate_code
7 | from classroom_manager import db
8 | from datetime import datetime
9 |
10 | users = {}
11 |
12 | @socketio.on('connect')
13 | def handle_connections():
14 | if not current_user.is_authenticated:
15 | disconnect()
16 | return redirect(url_for('login'))
17 | user = User.query.filter(User.id==current_user.get_id()).first()
18 | users[user.username] = request.sid
19 | user_memberships = Membership.query.filter(Membership.user_id==current_user.get_id())
20 | [join_room(str(user_membership.classroom_id)) for user_membership in user_memberships] #Instead of making a room for every channel we will make a room for every classroom
21 |
22 | @socketio.on('disconnect')
23 | def handle_disconnections():
24 | if not current_user.is_authenticated:
25 | disconnect()
26 | return redirect(url_for('login'))
27 |
28 | @socketio.on('channel_conversation')
29 | def handle_conversations(data):
30 | if current_user.is_authenticated:
31 | if len(data['message']):
32 | sender_id = current_user.get_id()
33 | sender = User.query.filter(User.id==sender_id).first()
34 | #broadcastting message to users in this room
35 | room = Channel.query.filter(Channel.id==data['channel_id']).first()
36 | if Membership.query.filter(Membership.user_id==sender_id, Membership.classroom_id==room.classroom_id).first():
37 | new_message = Message(author_id=sender_id, channel_id=data['channel_id'], contents=data['message'])
38 | emit('channel_conversation', {'content': data['message'], 'date': 'just now', 'author': {'id':sender_id, 'name': sender.first_name + ' ' + sender.last_name}, 'channel_id': data['channel_id']}, room=str(room.classroom_id))
39 | #Saving the message in the db
40 | db.session.add(new_message) #adding message
41 | db.session.commit()
42 | else:
43 | socket.emit('error', 'message must have more than one character.')
44 |
45 | else:
46 | disconnect()
47 | return redirect(url_for('login'))
48 |
49 | @socketio.on('join-room')
50 | def joinroom(room_id):
51 | if current_user.is_authenticated:
52 | join_room(str(room_id))
53 |
54 | @socketio.on('channel_action')
55 | def channel_action(data):
56 | membership = Membership.query.filter(Membership.user_id==current_user.get_id(), Membership.classroom_id==data['classroom_id']).first()
57 | if membership.role == 'super':
58 | if data['action'] == 'add':
59 | new_channel = Channel(classroom_id=int(data['classroom_id']),name=data['name_input'])
60 | db.session.add(new_channel)
61 | db.session.flush()
62 | db.session.commit()
63 | emit('new_channel', {'classroom_id': data['classroom_id'],'name': new_channel.name, 'id': new_channel.id},room=str(data['classroom_id']))
64 | elif data['action'] == 'rename':
65 | specified_channel = Channel.query.filter(Channel.id==data['channel_id']).first()
66 | specified_channel.name = data['name_input']
67 | db.session.commit()
68 | emit('channel_rename', {'classroom_id':data['classroom_id'],'new_name': data['name_input'], 'id': data['channel_id']}, room=str(data['classroom_id']))
69 | elif data['action'] == 'delete':
70 | specified_channel = Channel.query.filter(Channel.id==data['channel_id']).delete()
71 | Message.query.filter(Message.channel_id==data['channel_id']).delete()
72 | Note.query.filter(Note.channel_id==data['channel_id']).delete()
73 | db.session.commit()
74 | emit('channel_delete', {'classroom_id':data['classroom_id'],'id': data['channel_id']}, room=str(data['classroom_id']))
75 | else:
76 | pass
77 |
78 | @socketio.on('code_regeneration_req')
79 | def regenerate_code(data):
80 | Classroom.query.filter(Classroom.id==data['classroom_id']).first().code=generate_code(data['classroom_id'])
81 | db.session.commit()
82 | emit('code_regeneration', {'classroom_id': data['classroom_id'], 'code':Classroom.query.filter(Classroom.id==data['classroom_id']).first().code})
83 |
84 | @socketio.on('user_action')
85 | def user_action(data):
86 | membership = Membership.query.filter(Membership.user_id==current_user.get_id(), Membership.classroom_id==data['classroom_id']).first()
87 | if membership.role == 'super':
88 | if data['action'] == 'kick':
89 | Membership.query.filter(Membership.classroom_id==int(data['classroom_id']), Membership.user_id==int(data['user_id'])).delete()
90 | db.session.commit()
91 | emit('user_kick', {'user_id': data['user_id'], 'classroom_id': data['classroom_id']}, room=str(data['classroom_id']))
92 | elif data['action'] == 'ban':
93 | pass
94 |
95 | @socketio.on('classroom_leave')
96 | def user_leave(data):
97 | if current_user.is_authenticated:
98 | Membership.query.filter(Membership.user_id==current_user.get_id(), Membership.classroom_id==data['classroom_id']).delete()
99 | db.session.commit()
100 |
101 | @socketio.on('direct_message')
102 | def direct_message(data):
103 | new_direct_message = DirectMessage(sender_id=current_user.get_id(), receiver_id=data['to'], content=data['message'])
104 | db.session.add(new_direct_message)
105 | db.session.commit()
106 | recipient_username = User.query.filter(User.id==data['to']).first().username
107 | if recipient_username in users.keys():
108 | emit('direct_message', {'content': data['message'], 'author': User.query.filter(User.id==current_user.get_id()).first().username, 'date': 'Just now'}, room=users[recipient_username])
109 | emit('direct_message', {'content': data['message'], 'author': User.query.filter(User.id==current_user.get_id()).first().username, 'date': 'Just now'}, room=users[User.query.filter(User.id==current_user.get_id()).first().username])
--------------------------------------------------------------------------------
/classroom_manager/routes.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, render_template, url_for, flash, redirect, request, jsonify
2 | from classroom_manager.forms import RegistrationForm, LoginForm
3 | from classroom_manager import app, bcrypt, db, socketio
4 | from classroom_manager.models import User, Classroom, Membership, Channel, Message, Note, Assignment, DirectMessage, AssignmentSubmission
5 | from flask_login import login_user, current_user, logout_user, login_required
6 | from classroom_manager.utils import generate_code
7 | from werkzeug.utils import secure_filename
8 | from types import SimpleNamespace
9 | from dateutil import parser
10 | import os
11 |
12 | @app.route("/")
13 | @app.route("/home")
14 | def home():
15 | return render_template("home.html", title='Home Page')
16 |
17 | @app.route("/login", methods=["GET", "POST"]) # Make sure the form tag has the method, "POST"
18 | def login():
19 | if current_user.is_authenticated:
20 | return redirect(url_for("home"))
21 | form = LoginForm()
22 | if form.validate_on_submit():
23 | user = User.query.filter_by(email=form.email.data).first()
24 | if user and bcrypt.check_password_hash(user.password, form.password.data):
25 | login_user(user, remember=form.remember.data)
26 | next_page = request.args.get('next')
27 | flash(f"Successfully logged in!", 'success')
28 | return redirect(next_page) if next_page else redirect(url_for('__app__'))
29 | else:
30 | flash("Login unsuccessful, please try again.", 'danger')
31 | return render_template("login.html", form=form, title='Login')
32 |
33 | @app.route("/register", methods=["GET", "POST"]) # Make sure the form tag has the method, "POST"
34 | def register():
35 | if current_user.is_authenticated:
36 | return redirect(url_for("__app__"))
37 | form = RegistrationForm()
38 | if form.validate_on_submit():
39 | hashed_password = bcrypt.generate_password_hash(form.password.data).decode("utf-8") # Hash the password
40 | new_user = User(first_name=form.name.data, last_name=form.last_name.data, username=form.username.data,
41 | email=form.email.data, status=form.student_or_teacher.data, password=hashed_password)
42 | db.session.add(new_user) # Add the new user to the database queue
43 | db.session.commit() # Commit all the changes in the queue
44 | return redirect(url_for('login'))
45 | return render_template("register.html", form=form, title='Register')
46 |
47 | @app.route("/logout")
48 | def logout():
49 | logout_user()
50 | return redirect(url_for("home"))
51 |
52 | @app.route("/app")
53 | @login_required
54 | def __app__():
55 | return render_template("app.html", title='Application')
56 |
57 | @app.route('/app/chats', methods=['POST'])
58 | def app_chats():
59 | users, contacts = set(), []
60 | messages = DirectMessage.query.filter((DirectMessage.receiver_id==current_user.get_id()) | (DirectMessage.sender_id==current_user.get_id())).all()
61 | [users.add(message.receiver_id) if int(message.receiver_id) != int(current_user.get_id()) else users.add(int(message.sender_id)) for message in messages]
62 | for user in User.query.filter(User.id.in_(users)).all():
63 | contact = SimpleNamespace()
64 | contact.id = user.id
65 | contact.name = user.username
66 | contacts.append(contact)
67 | return render_template('chats.html', title='Contacts', data=contacts, type='contact')
68 |
69 | @app.route('/add-contact', methods=['POST'])
70 | def add_contact():
71 | contact = User.query.filter(User.username==request.form['username']).first()
72 | if contact and int(contact.id) == int(current_user.get_id()):
73 | contact = None #
74 | return jsonify({'name': contact.username, 'id': contact.id, 'img_url': url_for('static', filename='imgs/defaultuser-icon.png')}) if contact and contact.id != current_user.get_id() else jsonify({'error': 'user not found.'})
75 |
76 | @app.route('/retrieve-directmessages/', methods=['POST'])
77 | def retrieve_directmessages(user_id):
78 | messages = DirectMessage.query.filter((DirectMessage.sender_id==user_id) & (DirectMessage.receiver_id==current_user.get_id()) | (DirectMessage.sender_id==current_user.get_id()) & (DirectMessage.receiver_id==user_id))
79 | return jsonify({'messages': [{'content': message.content, 'date': message.date, 'author': User.query.filter(User.id==message.sender_id).first().username, 'mein': int(current_user.get_id())==(message.sender_id)} for message in messages]})
80 |
81 | '''
82 | @app.route('/retrieve-contacts', methods=['POST'])
83 | def retrieve_contacts():
84 | pass'''
85 |
86 | @app.route('/app/classrooms', methods=['POST'])
87 | def app_classrooms():
88 | memberships = Membership.query.filter(Membership.user_id==current_user.get_id()).all()
89 | classrooms = Classroom.query.filter(Classroom.id.in_([membership.classroom_id for membership in memberships])).all()
90 | return render_template('classrooms.html', title='Classrooms', data=classrooms, type='classroom')
91 |
92 | @app.route('/add-note', methods=['POST'])
93 | def add_note():
94 | if request.form['note_title'] and request.form['note_text'] and request.form['channel_id']:
95 | note_title = request.form['note_title']
96 | note_text = request.form['note_text']
97 | note_channel_id = request.form['channel_id']
98 | if request.files["note_image"]:
99 | image = request.files["note_image"]
100 | filename = secure_filename(image.filename)
101 | image.save(os.path.join(app.config['IMAGE_UPLOADS'], filename))
102 | new_note = Note(author_id=current_user.get_id(), title=note_title, note_text=note_text,note_imgs=filename, channel_id=note_channel_id)
103 | db.session.add(new_note)
104 | db.session.commit()
105 | return jsonify({'new_note': {'note_title':new_note.title, 'note_id': new_note.id, 'note_text': new_note.note_text, 'note_img': url_for('static', filename='imgs/' + new_note.note_imgs)}})
106 | else:
107 | pass
108 |
109 | @app.route('/classroom-settings/', methods=['POST'])
110 | def classroom_settings(classroom_id):
111 | specified_classroom = Classroom.query.filter(Classroom.id==classroom_id).first()
112 | if specified_classroom:
113 | membership = Membership.query.filter(Membership.classroom_id==classroom_id, Membership.user_id==current_user.get_id()).first()
114 | members = Membership.query.filter(Membership.classroom_id==classroom_id)
115 | members = User.query.filter(User.id.in_([member.user_id for member in members]))
116 | permission = membership.role == 'super'
117 | return jsonify({'room_code': specified_classroom.code,
118 | 'permission': permission,
119 | 'members': [{'name': member.first_name + ' ' + member.last_name,
120 | 'id': member.id} for member in members if int(member.id) != int(current_user.get_id())],
121 | 'channels': [{'id': channel.id, 'name': channel.name} for channel in Channel.query.filter(Channel.classroom_id==classroom_id)]})
122 | return jsonify({'error': 'Classroom wasn\'t found.'})
123 |
124 | @app.route('/create-team', methods=['POST'])
125 | def create_team():
126 | #getting data from the form
127 | team_name = request.form['team_name']
128 | team_description = request.form['team_description']
129 | if team_name:
130 | #creating the classroom
131 | new_classroom = Classroom(name=team_name, description=team_description)
132 | db.session.add(new_classroom) #adding the new classroom
133 | db.session.flush() #using flush just so we can get the id of the classroom
134 | new_classroom.code = generate_code(new_classroom.id)
135 | db.session.flush()
136 | new_channel = Channel(name="General", classroom_id=new_classroom.id) #creating general channel for the classroom
137 | db.session.add(new_channel)
138 | db.session.flush() #same here...
139 | #Creating membership for the user and classroom
140 | new_membership = Membership(user_id=current_user.get_id(), classroom_id=new_classroom.id, role='super')
141 | db.session.add(new_membership)
142 | db.session.commit()
143 | #sending back the classroom object
144 | return jsonify({'new_classroom': {
145 | 'name': new_classroom.name,
146 | 'id': new_classroom.id,
147 | 'imgurl': url_for('static', filename="imgs/" + new_classroom.image_file)
148 | },
149 | 'channel': new_channel.id
150 | })
151 | return jsonify({'error': 'given name is invalid.'}) #Returning an error if the given name is empty
152 |
153 |
154 | @app.route('/join-team', methods=['POST'])
155 | def join_team():
156 | team_code = request.form['code']
157 | if team_code:
158 | classroom_id = Classroom.query.filter(Classroom.code==team_code).first()
159 | if classroom_id:
160 | new_membership = Membership(user_id=current_user.get_id(), classroom_id=classroom_id.id, role='regular')
161 | db.session.add(new_membership)
162 | db.session.commit()
163 | return jsonify({'result': {'id': classroom_id.id,
164 | 'name': classroom_id.name},
165 | 'url_for_img': url_for('static', filename='imgs/'+ classroom_id.image_file)})
166 | return jsonify({'error': "given code has either expired or is not valid"})
167 | return jsonify({'error': 'given code is invalid'})
168 |
169 | @app.route('/retrieve-notes/', methods=['POST'])
170 | def retrieve_notes(channel):
171 | notes = Note.query.filter(Note.channel_id==channel)
172 | return jsonify([{'author_id': note.author_id, 'note_title': note.title,'note_id': note.id,'note_text': note.note_text, 'note_img': url_for('static', filename='imgs/'+note.note_imgs) if note.note_imgs else None} for note in notes])
173 |
174 | @app.route('/retrieve-assignments/', methods=['POST'])
175 | def retrieve_assignments(channel):
176 | assignments = Assignment.query.filter(Assignment.channel_id==channel)
177 | role = Membership.query.filter(Membership.classroom_id==Channel.query.filter(Channel.id==channel).first().classroom_id, Membership.user_id==current_user.get_id()).first().role
178 | return jsonify({'role': role, 'assignments': [{'id': assignment.id, 'text': assignment.assignment_text, 'duedate': assignment.due_date, 'submission_state': AssignmentSubmission.query.filter(assignment.id==AssignmentSubmission.assignment_id, AssignmentSubmission.user_id==current_user.get_id()).first() is not None} for assignment in assignments]})
179 |
180 | @app.route('/add-assignment', methods=['POST'])
181 | def add_assignment():
182 | get_date = parser.parse(request.form['assignment_date'])
183 | new_assignment = Assignment(author_id=current_user.get_id(), due_date=get_date, assignment_text=request.form['assignment_text'], channel_id=request.form['channel_id'])
184 | db.session.add(new_assignment)
185 | db.session.commit()
186 | return jsonify({'id': new_assignment.id, 'text': new_assignment.assignment_text, 'duedate': new_assignment.due_date})
187 |
188 | @app.route('/homework-submit', methods=['POST'])
189 | def homework_submit():
190 | if request.files["homework"]:
191 | homework_file = request.files["homework"]
192 | filename = secure_filename(homework_file.filename)
193 | homework_file.save(os.path.join(app.config['FILE_UPLOADS'], filename))
194 | new_submission = AssignmentSubmission(user_id=current_user.get_id(),assignment_id=request.form['assignment_id'], file_location=filename)
195 | db.session.add(new_submission)
196 | db.session.commit()
197 | return jsonify({'message': 'successfuly submitted'})
198 |
199 | @app.route('/retrieve-submissions/', methods=['POST'])
200 | def retrieve_submissions(assignment_id):
201 | assignments = AssignmentSubmission.query.filter(AssignmentSubmission.assignment_id==assignment_id)
202 | return jsonify([{'name': User.query.filter(User.id==assignment.user_id).first().username, 'file': url_for('static', filename='submissions/'+str(assignment.file_location))} for assignment in assignments])
203 |
204 |
205 | @app.route('/retrieve-channels/', methods=['POST'])
206 | def retrieve_channels(classroom):
207 | channels = Channel.query.filter(classroom==Channel.classroom_id)
208 | return jsonify({ 'result': [ { 'id': channel.id, 'name': channel.name } for channel in channels ] })
209 |
210 | @app.route('/retrieve-messages/', methods=['POST'])
211 | def retrieve_messages(channel):
212 | POSTS_COUNT_ONLOAD = 50
213 | messages = Message.query.filter(Message.channel_id==channel, Message.ischild==-1)
214 | return jsonify({'result': [ { 'id': message.id,
215 | 'content': message.contents,
216 | 'date': message.date,
217 | 'author': {
218 | 'id': message.author_id,
219 | 'name': User.query.filter(User.id==message.author_id)[0].first_name + ' ' + User.query.filter(User.id==message.author_id)[0].last_name,
220 | 'avatar':url_for('static', filename="imgs/" + User.query.filter(User.id==message.author_id)[0].image_file)}
221 | } for message in messages][-POSTS_COUNT_ONLOAD:] #returning the last ten messages in this channel
222 | })
223 |
224 | @app.route('/app/activity', methods=['POST'])
225 | def app_activity():
226 | messages = Message.query.filter(Message.author_id==current_user.get_id())
227 | return render_template('activity.html', data=messages)
228 |
229 | @app.route('/app/meetings', methods=['POST'])
230 | def app_meetings():
231 | return render_template('meetings.html')
232 |
233 | @app.route('/app/calls', methods=['POST'])
234 | def app_calls():
235 | return render_template('calls.html')
--------------------------------------------------------------------------------
/classroom_manager/site.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/site.sqlite3
--------------------------------------------------------------------------------
/classroom_manager/static/css/home.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif
5 | }
6 | .bgImage{
7 | width: 100%;
8 | height: 100vh;
9 | background-size: cover;
10 | background-position: center;
11 | z-index: -1111;
12 | }
13 |
14 | .nav_bar {
15 | width: 100%;
16 | height: 70px;
17 | color: #fff;
18 | border-bottom: 1px solid rgba(255,255,255,0.1);
19 | position: fixed;
20 | z-index: 1111;
21 | }
22 |
23 | .nav_bar h1 {
24 | margin: 0;
25 | padding: 0;
26 | text-transform: uppercase;
27 | float: left;
28 | line-height: 70px;
29 | margin-left: 70px;
30 | }
31 |
32 | nav {
33 | float: right;
34 | }
35 |
36 | nav ul {
37 | margin: 0;
38 | padding: 0;
39 | }
40 |
41 | nav ul li {
42 | list-style: none;
43 | display: inline-block;
44 | }
45 |
46 | nav ul li a {
47 | text-transform: uppercase;
48 | text-decoration: none;
49 | color: #fff;
50 | padding: 0 30px;
51 | line-height: 70px;
52 | display: block;
53 | transition: .4s;
54 | }
55 |
56 | nav ul li a:hover {
57 | color: #e74c3c;
58 | transition: .4s;
59 | }
60 |
61 | .overay {
62 | width: 100%;
63 | height: 100%;
64 | background: rgba(0,0,0,.6);
65 | }
66 |
67 | .text {
68 | position: absolute;
69 | top: 40%;
70 | left: 10%;
71 | color: #fff;
72 | z-index: 22222;
73 | width: 500px;
74 | height: 300px;
75 | background: transparent;
76 | }
77 |
78 | .text:before {
79 | content: '';
80 | position: absolute;
81 | top: -5px;
82 | left: 7%;
83 | transform: translate(-50%, -50%);
84 | width: 70px;
85 | height: 5px;
86 | background: #e74c3c;
87 | }
88 |
89 | .text h1 {
90 | margin: 0;
91 | padding: 0;
92 | font-weight: 700;
93 | color: #e74c3c;
94 | font-size: 50px;
95 | }
96 |
97 | .text h1 span {
98 | font-weight: normal;
99 | color: #F2F2F2;
100 | }
101 |
102 | .text p {
103 | padding: 20px 50px 20px 0;
104 | line-height: 22px;
105 | color: #F2F2F2;
106 | text-transform: uppercase;
107 | }
108 |
109 | button {
110 | border: none;
111 | background: transparent;
112 | color: #fff;
113 | border: 1px solid #fff;
114 | font-size: 16px;
115 | padding: 10px 30px;
116 | border-radius: 5px;
117 | transition: .3s;
118 | text-transform: uppercase;
119 | }
120 |
121 | .get-now {
122 | background: #e74c3c;
123 | border: 1px solid #e74c3c;
124 | margin-right: 20px;
125 | }
126 |
127 | .get-now:hover {
128 | border: 1px solid #fff;
129 | background: #fff;
130 | color: #000;
131 | transition: .3s;
132 | }
133 |
134 | .book-place:hover {
135 | background: #e74c3c;
136 | border: 1px solid #e74c3c;
137 | transition: .3s;
138 | }
--------------------------------------------------------------------------------
/classroom_manager/static/css/styles.css:
--------------------------------------------------------------------------------
1 | .searchbar {
2 | width: 100%;
3 | border-radius: 3px;
4 | height: 1.8em;
5 | }
6 | .selected_item {
7 | background-color: rgb(71,72, 90);
8 | }
9 | .navbar {
10 | background-color: rgb(179, 86, 86);
11 | }
12 | .sidebar {
13 | margin-top: 10px;
14 | width: 7.2em;
15 | color:white;
16 | font-size: 10px;
17 | text-align: center;
18 | background-color: rgb(117, 31, 25);
19 | display:block;
20 | }
21 | #app_container {
22 | margin:1px;
23 | margin-left: 4.8em;
24 | padding-top: 0.6em;
25 | display:block;
26 | }
27 | #sidebartitle {
28 | margin-left: 0.8em;
29 | }
30 | #app_body {
31 | margin-left: 18%;
32 | display:block;
33 | }
34 | .w3-sidebar {
35 | display:block;
36 | margin-right: 10px;
37 | }
38 |
39 | .w3-button {
40 | width:100%;
41 | }
42 | .w3-bar-item {
43 | width: 100%;
44 | }
45 | .channel_display {
46 | margin-left: 4px;
47 | display:block;
48 | }
49 | .messagescontainer {
50 | width: 100%;
51 | border-radius: 5px;
52 | min-height: 460px;
53 | max-height: 80vh;
54 | overflow-y: scroll;
55 | position: relative;
56 | display:block;
57 | }
58 | .user_chat_container {
59 | width: 100%;
60 | border-radius: 5px;
61 | min-height: 600px;
62 | max-height: 85vh;
63 | overflow-y: scroll;
64 | position: relative;
65 | display:block;
66 | }
67 | .tab-content {
68 | width: 100%;
69 | }
70 | .channel_display h4 {
71 | min-width: 100px;
72 | width: 700px;
73 | border-radius: 5px;
74 | margin-top: 0.6em;
75 | margin-bottom: 0.3em;
76 | }
77 | .message {
78 | padding: 10px;
79 | margin-bottom: 15px;
80 | background-color: rgba(49, 170, 168, 1);
81 | border-radius: 4px;
82 | border-left: 4px solid grey;
83 | width: 99.99%;
84 | }
85 | .my_second_sidebar {
86 | min-width: 100px;
87 | width: 14%;
88 | }
89 | .textinput {
90 | padding: 4px;
91 | margin: 4px;
92 | border-color: white;
93 | border-bottom: 4px solid rgb(70,71,117);
94 | border-radius: 4px;
95 | width: 99.99%;
96 | color: black;
97 | }
98 | #new_conversation_form {
99 | display: none;
100 | }
101 | .upper_part {
102 | font-size: 18px;
103 | margin-top: 2px;
104 | }
105 | .fas {
106 | color:black;
107 | }
108 | .fas:hover {
109 | color:white;
110 | }
111 | #note_form {
112 | margin-top: 1em;
113 | }
114 | .btn {
115 | z-index: 0;
116 | }
117 | /* The Overlay (background) */
118 | .overlay {
119 | /* Height & width depends on how you want to reveal the overlay (see JS below) */
120 | height: 100%;
121 | width: 0;
122 | position: fixed; /* Stay in place */
123 | z-index: 1; /* Sit on top */
124 | left: 0;
125 | top: 0;
126 | background-color: rgb(0,0,0); /* Black fallback color */
127 | background-color: rgba(0,0,0, 0.9); /* Black w/opacity */
128 | overflow-x: hidden; /* Disable horizontal scroll */
129 | transition: 0.5s; /* 0.5 second transition effect to slide in or slide down the overlay (height or width, depending on reveal) */
130 | }
131 | .overlay-content {
132 | position: relative;
133 | top: 10%;
134 | width: 100%;
135 | text-align: center;
136 | margin-top: 30px;
137 | }
138 | .overlay a {
139 | padding: 8px;
140 | text-decoration: none;
141 | font-size: 36px;
142 | color: #818181;
143 | display: block;
144 | transition: 0.3s;
145 | }
146 | .overlay a:hover, .overlay a:focus {
147 | color: #f1f1f1;
148 | }
149 | .overlay .closebtn {
150 | position: absolute;
151 | top: 20px;
152 | right: 45px;
153 | font-size: 60px;
154 | }
155 | .overlay-content p{
156 | color:white;
157 | font-size: 24px;
158 | }
159 | .action-container {
160 | width:30%;
161 | margin: auto;
162 | }
163 | .action-title {
164 | font-weight: bold;
165 | }
166 | .user-avatar {
167 | border-radius: 4px;
168 | }
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/1_vYtDyAbOMTJnArJvXix5wg.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/1_vYtDyAbOMTJnArJvXix5wg.jpeg
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/7bbcc1707ce09218fa84866a65b3c871.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/7bbcc1707ce09218fa84866a65b3c871.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/800px_COLOURBOX4159486.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/800px_COLOURBOX4159486.jpg
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/Periodic_Table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/Periodic_Table.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/call-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/call-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/chat-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/chat-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/default_class.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/default_class.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/defaultuser-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/defaultuser-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/dropdown-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/dropdown-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/gear-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/gear-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/group-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/group-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/meetings-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/meetings-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/notification-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/notification-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/plus-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/plus-icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/storage-nand_nor_gates-f_mobile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/storage-nand_nor_gates-f_mobile.png
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/trig.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/trig.gif
--------------------------------------------------------------------------------
/classroom_manager/static/imgs/website_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/classroom_manager/static/imgs/website_icon.png
--------------------------------------------------------------------------------
/classroom_manager/static/js/app.js:
--------------------------------------------------------------------------------
1 | /* Client script */
2 | document.body.style.zoom = 0.8;
3 |
4 | const app_container = $('#app_container');
5 | const app_body = app_container.children("#app_body");
6 | let selected_page = null;
7 | const socket = io.connect('http://' + document.domain + ':' + location.port);
8 | socket.on('connect', function() {
9 | console.log("Connected to the network server.");
10 | });
11 |
12 | $(document).ready(function() {
13 | $('#mySidebar .w3-bar-item').on('click', function(event) {
14 | let this_element = this;
15 | if($(this).attr('name') != selected_page){
16 | selected_page = $(this).attr('name');
17 | $.ajax({url: '/app/' + $(this).attr('name'), type: 'POST'}).done(function (data) {
18 | app_container.html(data);
19 | $(this_element).addClass('selected_item');
20 | $('#mySidebar > .w3-bar-item').each(function(index){
21 | if (this !== this_element){
22 | $(this).removeClass('selected_item');
23 | }
24 | });
25 | });
26 | }
27 | });
28 | $('#joinclassform').on('submit', function(event) {
29 | event.preventDefault();
30 |
31 | $.ajax({data: {code: $('#classroom_code').val()}, type:'POST', url:'/join-team'}).done(function(data) {
32 | if (data.error) {
33 | console.log(data.error);
34 | $('#join_notice').css('display', 'block');
35 | $('#join_notice').html(data.error);
36 | }
37 | else {
38 | $('#menulist').append(``);
39 | socket.emit('join-room', data.result.id);
40 | }
41 | });
42 | });
43 | $('#teamcreationform').on('submit', function(event) {
44 | $.ajax({
45 | data: {
46 | team_name: $('#teamname').val(),
47 | team_description: $('#teamdescription').val()
48 | },
49 | type: 'POST',
50 | url: '/create-team'
51 | }).done(function(data) {
52 | const notice_field = $('#notice');
53 | if (data.error) {
54 | notice_field.html(data.error);
55 | notice_field.addClass('alert-danger');
56 | notice_field.css('display', 'block');
57 | console.log("error: team was not created due to the following < " + data.error + " >");
58 | }
59 | else {
60 | notice_field.css('display', 'block');
61 | notice_field.addClass('alert-success');
62 | notice_field.html("classroom `" + $('#teamname').val() + "` has been created.");
63 | socket.emit('join-room', data.new_classroom.id);
64 | $('#teamname').val('');
65 | $('#teamdescription').val('');
66 | setTimeout(() => {
67 | notice_field.html('');
68 | notice_field.hide();
69 | $("#teammodal").hide();
70 | }, 1624);
71 | if($('#classrooms').hasClass('selected_item')) {
72 | $('#menulist').append(``);
73 | }
74 | }
75 | });
76 | event.preventDefault();
77 | });
78 | });
79 |
80 | const toggle_class_creator = () => {
81 | $('#teammodal').css('display', 'block');
82 | $('#teammodal').find('.alert').css('display', 'none');
83 | }
84 | const toggle_class_join = () => {
85 | $('#joinclass_modal').css('display', 'block');
86 | $('#join_notice').css('display', 'none');
87 | }
88 | const close_settings_menu = () => {
89 | $('#classroom_settings').css('width', '0%');
90 | }
--------------------------------------------------------------------------------
/classroom_manager/static/js/chat.js:
--------------------------------------------------------------------------------
1 | selected_user = null;
2 |
3 | const toggle_username_input = () => {
4 | if($('#username_input_id').is(':visible')) {
5 | $('#username_input_id').hide();
6 | }
7 | else {
8 | $('#username_input_id').show();
9 | }
10 | }
11 | const add_user_message = (message) => {
12 | $('#user_chat_box').append(`${message.author} ${message.date} ${message.content}
`);
13 | $('#user_chat_box').scrollTop(10000);
14 |
15 | }
16 | const set_user_chat_title = (title) => {
17 | $('#chat_title').html(title);
18 | }
19 | const clear_chat = () => {
20 | $('#user_chat_box').html('');
21 | }
22 | $(document).on('submit', '#username_input_id', function(event) {
23 | event.preventDefault();
24 | $.ajax({url:'/add-contact', type:'POST', data: {'username': $('#username_input').val()}}).done(function(data) {
25 | if(data.error) {
26 | console.log(data.error);
27 | }
28 | else {
29 | $('#menulist').append(``);
30 | }
31 | });
32 | $('#username_input').val('');
33 | });
34 | $(document).on('click', '.user', function() {
35 | id = parseInt($(this).attr('id').slice(1, $(this).attr('id').length), 10);
36 | selected_user = id;
37 | this_element = $(this);
38 | $.ajax({url:`retrieve-directmessages/${id}`, type:'POST'}).done(function(data) {
39 | console.log(data);
40 | clear_chat();
41 | set_user_chat_title(this_element.html());
42 | for(let i = 0; i < data.messages.length; i++) {
43 | add_user_message(data.messages[i]);
44 | }
45 | });
46 | });
47 | $(document).on('submit', '#message_input', function(e) {
48 | e.preventDefault();
49 | message = $('#message_input input').val()
50 | if (selected_user && message) {
51 | socket.emit('direct_message', {'to': selected_user, 'message': message});
52 | }
53 | $(this).children('input').val('');
54 | });
55 | socket.on('direct_message', function(data) {
56 | add_user_message(data);
57 | });
--------------------------------------------------------------------------------
/classroom_manager/static/js/classroom.js:
--------------------------------------------------------------------------------
1 | selected_channel = null;
2 | selected_classroom = null;
3 | const clear_chatbox = () => {
4 | const chat_box = $('#channel_display .messagescontainer').first();
5 | chat_box.html('');
6 | }
7 | const set_chat_title = (server_title,channel_title) => {
8 | const chat_title = $('#channel_display .upper_part .title').first(); //getting the first h4 element inside of the channel_display element
9 | chat_title.html(`${server_title} ${channel_title}`);
10 | $(chat_title).find('i').remove();
11 | }
12 | const clear_submissions = () => {
13 | $('#submissions').html('');
14 | }
15 | const add_message = (message_data) => {
16 | const chat_box = $('#channel_display .messagescontainer').first();
17 | chat_box.append(`${message_data.author.name} ${message_data.date}${message_data.content}
`);
18 | $(chat_box).scrollTop(10000);
19 | }
20 | const clear_notes = () => {
21 | const notes = $('#note-container');
22 | const notes_list = $ ('#note-list');
23 | notes.html('');
24 | notes_list.html('');
25 | }
26 | const add_note = (note_data, active) => {
27 | const note_list = $('#note-list');
28 | const note_container = $('#note-container');
29 | let id = note_data.note_title.replaceAll(' ', '') + note_data.note_id.toString(10);
30 | note_list.append(`${note_data.note_title}`);
31 | additional_text = '';
32 | if(note_data.note_img) {
33 | additional_text += `
`;
34 | }
35 | note_container.append(`${additional_text}
${note_data.note_text}
`);
36 | }
37 | const add_assignment = (assignment_data, is_super) => {
38 | const assignment_list = $('#assignments-list');
39 | if (assignment_data.id) {
40 | if(is_super) {
41 | assignment_list.append(`${assignment_data.text} | ${assignment_data.duedate} | |
`);
42 | }
43 | else {
44 | if (assignment_data.submission_state) {
45 | assignment_list.append(`${assignment_data.text} | ${assignment_data.duedate} | Already submitted |
`);
46 | }
47 | else {
48 | assignment_list.append(`${assignment_data.text} | ${assignment_data.duedate} | |
`);
49 |
50 | }
51 | }
52 | }
53 | }
54 | const clear_assignments = () => {
55 | const assignment_list = $('#assignments-list');
56 | assignment_list.html('');
57 | }
58 | const toggle_submissions_menu = () => {
59 | $('#testest').css('display', 'block');
60 | }
61 | $(document).ready(function() {
62 | $('body').click(function(e) {
63 | var target = $(e.target);
64 | if(!target.is('#new_conversation_form') && !target.is('#new_conversation_form input')) {
65 | is_visible = $('#new_conversation_form').css('display');
66 | if (is_visible != 'none') {
67 | $('#new_conversation_form').css('display', 'none');
68 | $('#newconversation').css('display', 'block');
69 | }
70 | }
71 | });
72 | $(document).on('submit', '#note_form', function(event) {
73 | let form_data = new FormData($('#note_form')[0]);
74 | form_data.append('channel_id', selected_channel);
75 | event.preventDefault();
76 | if(selected_channel != null) {
77 | $.ajax({data: form_data,contentType:false, processData:false, url:'add-note', method:'POST'}).done(function(data) {
78 | $("#note_form").trigger("reset");
79 | add_note(data['new_note'], '');
80 | });
81 | }
82 | });
83 | $(document).on('click','#menulist .classroom',function(){
84 | let id = parseInt($(this).attr('id').slice(1, $(this).attr('id').length), 10);
85 | selected_classroom = id;
86 | $.ajax({url:`retrieve-channels/${id}`, type:'POST'}).done((data) => {
87 | console.log(data);
88 | if(data['result'].length > 0){
89 | let result = '';
90 | for(let i = 0; i < data['result'].length; i++) {
91 | result+= `${data['result'][i].name}`;
92 | }
93 | $('#menulist ul').html('');
94 | $(this).parent().children('.channels').first().html(result);
95 | }
96 | });
97 | });
98 | $(document).on('click', '.classroom i', function() {
99 | $('#classroom_settings').css('width', '100%');
100 | let id = parseInt($(this).parent().attr('id').slice(1, $(this).parent().attr('id').length),10);
101 | $.ajax({url:`classroom-settings/${id}`, type:'POST'}).done(function(data) {
102 | console.log(data);
103 | $('#code_id').html(data['room_code']);
104 | $('#channel_selection').html('');
105 | $('#user_selection').html('');
106 | for(let i = 0; i < data.channels.length;i++) {
107 | $('#channel_selection').append(``);
108 | }
109 | for(let i = 0; i < data.members.length; i++) {
110 | $('#user_selection').append(``);
111 | }
112 | if(data.permission) {
113 | $('.permission').show();
114 | }
115 | else {
116 | $('.permission').hide();
117 | }
118 | });
119 | });
120 | $(document).on('click', '#channels_action_submission_button', function() {
121 | channel_id = $('#channel_selection').children("option:selected").val();
122 | socket.emit('channel_action', {'classroom_id': selected_classroom, 'action': $('#action_selection').children("option:selected").html(), 'channel_id': channel_id, 'name_input': $('#name_input').val()});
123 | });
124 | $(document).on('click', '#users_action_submission_button', function() {
125 | user_id = $('#user_selection').children("option:selected").val();
126 | socket.emit('user_action', {'classroom_id': selected_classroom, 'action': $('#action_selection_users').children("option:selected").html(), 'user_id': user_id});
127 | });
128 | $(document).on('change', '#action_selection', function() {
129 | let selectedAction = $(this).children("option:selected").html();
130 | if(selectedAction == 'rename' || selectedAction == 'add') {
131 | console.log('selected action is rename');
132 | $('#name_input').css('display', 'block');
133 | }
134 | else {
135 | $('#name_input').css('display', 'none');
136 | }
137 | });
138 | $(document).on('click', '.channels li', function() {
139 | let id = parseInt($(this).attr('id').slice(1, $(this).attr('id').length), 10); //getting channel id
140 | selected_channel = id;
141 | set_chat_title($(this).parent().parent().children('a').first().html(),$(this).html());
142 | $.ajax({url:`retrieve-messages/${id}`, type:'POST'}).done((data) => {
143 | clear_chatbox()
144 | if (data.result.length) {
145 | for(let i=0; i < data.result.length; i++) {
146 | add_message(data.result[i]);
147 | }
148 | } else {
149 | let message = {'author': {'name': 'SC. Bot'}, 'content': 'No messages to display', 'date': 'just now'};
150 | add_message(message);
151 | }
152 | });
153 | $.ajax({url:`retrieve-notes/${id}`, type:'POST'}).done((data) => {
154 | clear_notes();
155 | if(data.length) {
156 | for(let i = 0;i < data.length; i++) {
157 | if(i == 0) {
158 | add_note(data[i], 'active');
159 | }
160 | else {
161 | add_note(data[i], '');
162 | }
163 | }
164 | }
165 | else {
166 | new_note = {'note_title': 'No data', 'note_text': 'No data', 'note_id': -1};
167 | add_note(new_note,'active');
168 | }
169 | });
170 | $.ajax({url:`retrieve-assignments/${id}`, type:'POST'}).done((data) => {
171 | console.log(data);
172 | clear_assignments();
173 | if(data.assignments.length) {
174 | result = false;
175 | if(data.role == 'super') {
176 | result = true;
177 | }
178 | for(let i = 0; i < data.assignments.length; i++) {
179 | add_assignment(data.assignments[i], result);
180 | }
181 | }
182 | else {
183 | new_assignment = {'text': 'none', 'duedate': 'none', 'submission_state': '.'};
184 | add_assignment(new_assignment);
185 | }
186 | });
187 | });
188 | $(document).on('click', '#newconversation', function() {
189 | $('#new_conversation_form').css('display', 'block'); //showing new conversation form
190 | $('#newconversation').css('display', 'none'); //hiding newconversation button
191 | });
192 | $(document).on('keydown', '#new_conversation_form', function(event) {
193 | if (event.keyCode == 13) {
194 | event.preventDefault(); //preventing form from submitting
195 | const user_input = $('#new_conversation_input').val(); //assigning the input to a variable
196 | $('#new_conversation_input').val(''); //setting input value to empty string
197 | if (selected_channel != null) {
198 | socket.emit('channel_conversation', {'channel_id': selected_channel, 'message': user_input});
199 | }
200 | }
201 | });
202 | $(document).on('click', '#regenerate_code', function() {
203 | socket.emit('code_regeneration_req', {'classroom_id': selected_classroom});
204 | });
205 | $(document).on('click', '#leave_classroom', function() {
206 | socket.emit('classroom_leave', {'classroom_id': selected_classroom});
207 | set_chat_title('', 'No channel to display...');
208 | clear_notes();
209 | clear_chatbox();
210 | close_settings_menu();
211 | $(`[id='%${selected_classroom}']`).parent().remove();
212 | });
213 | $(document).on('submit', "#assignment_form", function(event) {
214 | event.preventDefault();
215 | $('#selected_channel_input').val(selected_channel);
216 | $.ajax({url:'/add-assignment', type:'POST', data: $('#assignment_form').serialize(), success: function(data) {
217 | add_assignment(data, true);
218 | }});
219 | });
220 | $(document).on('submit', '#homework_submission', function(event) {
221 | event.preventDefault();
222 | let form_data = new FormData($('#homework_submission')[0]);
223 | form_data.append('channel_id', selected_channel);
224 | form_data.append('assignment_id', parseInt($('#homework_submission').closest('tr').attr('value'), 10));
225 | event.preventDefault();
226 | if(selected_channel != null) {
227 | $.ajax({data: form_data,contentType:false, processData:false, url:'/homework-submit', method:'POST'}).done(function(data) {
228 | $('#homework_submission').html('Already submitted');
229 | });
230 | }
231 | });
232 | $(document).on('click', '.show_submission_button', function() {
233 | id = parseInt($(this).closest('tr').attr('value'), 10);
234 | console.log(id);
235 | $.ajax({url:`retrieve-submissions/${id}`, type:'POST'}).done(function(data) {
236 | clear_submissions();
237 | $('#submissions').append('Username | Homework File |
');
238 | for(i = 0; i < data.length; i++) {
239 | $('#submissions').append(`${data[i].name} | Download submission file |
`);
240 | }
241 | });
242 | });
243 | });
244 | socket.on('channel_conversation', function(data) {
245 | if(data.channel_id == selected_channel) {
246 | console.log(data);
247 | add_message(data);
248 | }
249 | else {
250 | //Code for notification goes here
251 | }
252 | });
253 | socket.on('channel_delete', function(data) {
254 | if(selected_channel == parseInt(data['id'], 10)) {
255 | set_chat_title('','No channel to display...');
256 | clear_notes();
257 | clear_chatbox();
258 | }
259 | if(selected_classroom == data['classroom_id']) {
260 | $(`[id='%${data['classroom_id']}']`).parent().children('ul').children().each(function() {
261 | if($(this).attr('id') == '$' + String(data['id'])){
262 | $(this).remove();
263 | $('#channel_selection').children().each(function() {
264 | if($(this).val() == parseInt(data['id'], 10)) {
265 | $(this).remove();
266 | }
267 | });
268 | }
269 | })
270 | }
271 | });
272 | socket.on('new_channel', function(data) {
273 | if(selected_classroom == data['classroom_id']) {
274 | $(`[id='%${data['classroom_id']}']`).parent().children('ul').append(`${data['name']}`);
275 | $('#channel_selection').append(``);
276 | }
277 | });
278 | socket.on('channel_rename', function(data) {
279 | if(selected_classroom == data['classroom_id']) {
280 | $(`[id='%${data['classroom_id']}']`).parent().children('ul').children().each(function() {
281 | if($(this).attr('id') == '$' + String(data['id'])) {
282 | $(this).html('' + data['new_name']);
283 | }
284 | });
285 | $('#channel_selection').children().each(function() {
286 | if($(this).val() == parseInt(data['id'], 10)) {
287 | $(this).html(data['new_name']);
288 | }
289 | });
290 | }
291 | });
292 | socket.on('code_regeneration', function(data) {
293 | if(selected_classroom == data['classroom_id']) {
294 | $('#code_id').html(data['code']);
295 | }
296 | });
297 | socket.on('user_kick', function(data) {
298 | console.log(data);
299 | if(parseInt(data['user_id'], 10) == parseInt($('#user_id').attr('value'), 10)) {
300 | $(`[id='%${data['classroom_id']}']`).parent().remove();
301 | if(selected_classroom == data['classroom_id']) {
302 | set_chat_title('','No channel to display...');
303 | clear_notes();
304 | clear_chatbox();
305 | }
306 | }
307 | });
--------------------------------------------------------------------------------
/classroom_manager/static/submissions/homeworktest.txt:
--------------------------------------------------------------------------------
1 | blah blah blah
--------------------------------------------------------------------------------
/classroom_manager/templates/activity.html:
--------------------------------------------------------------------------------
1 |
2 |
Your recent messages
3 |
4 | {% for message in data %}
5 |
You on {{message.date}}{{message.contents}}
6 | {% endfor %}
7 |
8 |
--------------------------------------------------------------------------------
/classroom_manager/templates/app.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block content %}
3 |
4 |
23 |
24 |
30 |
31 |
32 |
33 |
34 |
58 |
59 |
60 |
61 |
62 |
66 |
67 |
68 |
69 |
77 |
78 |
79 |
80 |
81 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {% endblock content %}
--------------------------------------------------------------------------------
/classroom_manager/templates/app_layout.html:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | {% block app %}{% endblock %}
22 |
--------------------------------------------------------------------------------
/classroom_manager/templates/calls.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/classroom_manager/templates/chats.html:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
No user chat to display...
28 |
29 |
30 |
SC. Bot Just nowNo messages to display.
31 |
32 |
33 |
36 |
--------------------------------------------------------------------------------
/classroom_manager/templates/classrooms.html:
--------------------------------------------------------------------------------
1 | {% extends "app_layout.html" %}
2 | {% block app %}
3 |
4 |
5 |
6 |
7 |
8 | No channel to display...
9 |
10 |
11 |
12 |
23 |
24 |
25 |
26 |
SC. Bot just now.No messages to display...
27 |
28 |
29 |
32 |
33 |
57 |
81 |
82 |
83 |
84 |
×
85 |
86 |
Classroom code: ...
87 |
88 |
89 |
90 |
91 |
92 |
93 |
Channels:
94 |
95 |
101 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
Users:
111 |
112 |
116 |
117 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | {% endblock app %}
127 |
--------------------------------------------------------------------------------
/classroom_manager/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Smart class - {{title}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Smart Class .
13 |
24 |
25 |
26 |
27 |
28 |
Smart Class
29 |
“eLearning doesn't just "happen"! It requires careful planning and implementation.”
~~Christopher Palm
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/classroom_manager/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {% if title %}
15 | Smart class - {{title}}
16 | {% else %}
17 | Smart class
18 | {% endif %}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {% block content %}{% endblock %}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/classroom_manager/templates/login.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block content %}
3 |
4 |
5 |
6 |
7 |
39 |
40 |
41 |
42 |
43 | {% endblock content %}
--------------------------------------------------------------------------------
/classroom_manager/templates/meetings.html:
--------------------------------------------------------------------------------
1 | Unfortunately this feature isn't implemented yet :(
--------------------------------------------------------------------------------
/classroom_manager/templates/register.html:
--------------------------------------------------------------------------------
1 | {% extends "layout.html" %}
2 | {% block content %}
3 |
4 |
5 |
6 |
7 |
63 |
64 |
65 |
66 |
67 | {% endblock content %}
--------------------------------------------------------------------------------
/classroom_manager/utils.py:
--------------------------------------------------------------------------------
1 | from random import randint
2 |
3 | def generate_code(classroom_id):
4 | return str(classroom_id) + '#' + ''.join([ chr(65 + randint(0,25)) for i in range(8)])
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask==1.1.2
2 | flask_sqlalchemy==2.4.4
3 | flask_bcrypt==0.7.1
4 | flask_login==0.5.0
5 | flask_socketio==4.3.1
6 | flask_wtf==0.14.3
7 | email_validator==1.1.2
8 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | from classroom_manager import socketio,app
2 |
3 | if __name__ == "__main__":
4 | socketio.run(app, debug=True,host='0.0.0.0', port=5000)
--------------------------------------------------------------------------------
/screenshots/screenshot1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/screenshots/screenshot1.PNG
--------------------------------------------------------------------------------
/screenshots/screenshot2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/screenshots/screenshot2.PNG
--------------------------------------------------------------------------------
/screenshots/screenshot3.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/screenshots/screenshot3.PNG
--------------------------------------------------------------------------------
/screenshots/screenshot4.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/screenshots/screenshot4.PNG
--------------------------------------------------------------------------------
/screenshots/screenshot5.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/screenshots/screenshot5.PNG
--------------------------------------------------------------------------------
/screenshots/screenshot6.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r1karim/classroom-manager/0b8d851f16195c480eda3c2dec44ad153d628b41/screenshots/screenshot6.PNG
--------------------------------------------------------------------------------