Version 1.0 ready (#1)

* FEAT: Basic app completed & file uploading added

* Added pdf/txt support and quiz template

* FEAT: PDF/TEXT uploading and getting file contents done

* Updated .gitignore

* Modified quiz template

* Fix UI - Added CSS to upload page

* Feat Quiz UI - Added CSS to Quiz Page

* Feat: Question Extraction - Extract questions from raw text

* Feat: added incorrect answer generation

* Feat: Tied together question extraction and incorrect anwer generation

* Fix: handle word not in vocabulary error

* Fix: added document as argument to get_questions_dict

* Feat: Integrated uploading file and question generation; needs fixes

* Feat Quiz UI - added correct answer UI

* Feat: Clean text for better processing

* Fix UI - added gradient and spacing

* Feat UI - Added uploading animation

* Fixed options issue on quiz screen

* Fix UI - added fonts, favicon etc

* Fix - Formatted code

* Feat UI - added fork me on github ribbon

* Feat: Added quiz result functionality

* Fix UI - added text to landing page

* Fix UI - added CSS to submit button

* Feat UI - added CSS to results page

Co-authored-by: user86 <kshitij.kotasthane@gmail.com>
Co-authored-by: telescopic <vigneshsuresh1775@gmail.com>
This commit is contained in:
Pragati Verma 2020-10-11 23:35:13 +05:30 committed by GitHub
parent 812a92bbea
commit 9d2adf338d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1188 additions and 2 deletions

141
.gitignore vendored Normal file
View File

@ -0,0 +1,141 @@
# Do not upload folder, auto created at each run
pdf/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/

View File

@ -1,4 +1,4 @@
# Fantastic-Falcons-1.0 # MLH Quizzet
This is a smart Quiz Generator that generates a dynamic quiz from any uploaded text/PDF document using NLP. This can be used for self-analysis, question paper generation, and evaluation, thus reducing human effort. This is a smart Quiz Generator that generates a dynamic quiz from any uploaded text/PDF document using NLP. This can be used for self-analysis, question paper generation, and evaluation, thus reducing human effort.
@ -21,7 +21,8 @@ This is a smart Quiz Generator that generates a dynamic quiz from any uploaded t
## Technology Stack: ## Technology Stack:
<img src="https://img.shields.io/badge/html5%20-%23E34F26.svg?&style=for-the-badge&logo=html5&logoColor=white"/> <img src="https://img.shields.io/badge/css3%20-%231572B6.svg?&style=for-the-badge&logo=css3&logoColor=white"/> <img src="https://img.shields.io/badge/javascript%20-%23323330.svg?&style=for-the-badge&logo=javascript&logoColor=%23F7DF1E"/> <img src="https://img.shields.io/badge/python%20-%2314354C.svg?&style=for-the-badge&logo=python&logoColor=white"/> <img src="https://img.shields.io/badge/flask%20-%23000.svg?&style=for-the-badge&logo=flask&logoColor=white"/> <img src="https://img.shields.io/badge/bootstrap%20-%23563D7C.svg?&style=for-the-badge&logo=bootstrap&logoColor=white"/> <img src="https://img.shields.io/badge/github%20-%23121011.svg?&style=for-the-badge&logo=github&logoColor=white"/> <img src ="https://img.shields.io/badge/sqlite-%2307405e.svg?&style=for-the-badge&logo=sqlite&logoColor=white"/>
<img src="https://img.shields.io/badge/html5%20-%23E34F26.svg?&style=for-the-badge&logo=html5&logoColor=white"/> <img src="https://img.shields.io/badge/css3%20-%231572B6.svg?&style=for-the-badge&logo=css3&logoColor=white"/> <img src="https://img.shields.io/badge/javascript%20-%23323330.svg?&style=for-the-badge&logo=javascript&logoColor=%23F7DF1E"/> <img src="https://img.shields.io/badge/python%20-%2314354C.svg?&style=for-the-badge&logo=python&logoColor=white"/> <img src="https://img.shields.io/badge/flask%20-%23000.svg?&style=for-the-badge&logo=flask&logoColor=white"/> <img src="https://img.shields.io/badge/bootstrap%20-%23563D7C.svg?&style=for-the-badge&logo=bootstrap&logoColor=white"/> <img src="https://img.shields.io/badge/github%20-%23121011.svg?&style=for-the-badge&logo=github&logoColor=white"/> <img src ="https://img.shields.io/badge/sqlite-%2307405e.svg?&style=for-the-badge&logo=sqlite&logoColor=white"/>
- **Frontend**: HTML, CSS, Vanilla JS - **Frontend**: HTML, CSS, Vanilla JS
- **Backend**: Flask - **Backend**: Flask
@ -107,6 +108,12 @@ $ python app.py
| 2. | Kshitij Kotasthane | Backend Developer | [@kshitij86](https://github.com/kshitij86) | | 2. | Kshitij Kotasthane | Backend Developer | [@kshitij86](https://github.com/kshitij86) |
| 3. | Vignesh S | ML | [@telescopic](https://github.com/telescopic) | | 3. | Vignesh S | ML | [@telescopic](https://github.com/telescopic) |
<br>
<br>
![Fantastic Falcons](https://user-images.githubusercontent.com/42115530/95672625-0aef1880-0bc0-11eb-8db6-90f6e16af2c0.gif)
## Contributors ✨ ## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

72
app.py Normal file
View File

@ -0,0 +1,72 @@
import os
from flask import Flask, render_template, redirect, url_for
from flask.globals import request
from werkzeug.utils import secure_filename
from workers import pdf2text, txt2questions
# Constants
UPLOAD_FOLDER = './pdf/'
# Init an app object
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# Global quiz object
questions = dict()
@ app.route('/')
def index():
""" The landing page for the app """
return render_template('index.html')
@ app.route('/quiz', methods=['GET', 'POST'])
def quiz():
""" Handle upload and conversion of file + other stuff """
UPLOAD_STATUS = False
# Make directory to store uploaded files, if not exists
if not os.path.isdir('./pdf'):
os.mkdir('./pdf')
if request.method == 'POST':
try:
# Retrieve file from request
uploaded_file = request.files['file']
file_path = os.path.join(
app.config['UPLOAD_FOLDER'],
secure_filename(
uploaded_file.filename))
file_exten = uploaded_file.filename.rsplit('.', 1)[1].lower()
# Save uploaded file
uploaded_file.save(file_path)
# Get contents of file
uploaded_content = pdf2text(file_path, file_exten)
questions = txt2questions(uploaded_content)
# File upload + convert success
if uploaded_content is not None:
UPLOAD_STATUS = True
except Exception as e:
print(e)
return render_template(
'quiz.html',
uploaded=UPLOAD_STATUS,
questions=questions,
size=len(questions))
@app.route('/result', methods=['POST', 'GET'])
def result():
correct_q = 0
for k, v in request.form.items():
correct_q += 1
return render_template('result.html', total=5, correct=correct_q)
if __name__ == "__main__":
app.run(debug=True)

View File

@ -0,0 +1,61 @@
''' This module contains the class
for generating incorrect alternative
answers for a given answer
'''
import gensim
import gensim.downloader as api
from gensim.models import Word2Vec
from nltk.tokenize import sent_tokenize, word_tokenize
import random
import numpy as np
class IncorrectAnswerGenerator:
''' This class contains the methods
for generating the incorrect answers
given an answer
'''
def __init__(self, document):
# model required to fetch similar words
self.model = api.load("glove-wiki-gigaword-100")
self.all_words = []
for sent in sent_tokenize(document):
self.all_words.extend(word_tokenize(sent))
self.all_words = list(set(self.all_words))
def get_all_options_dict(self, answer, num_options):
''' This method returns a dict
of 'num_options' options out of
which one is correct and is the answer
'''
options_dict = dict()
try:
similar_words = self.model.similar_by_word(answer, topn=15)[::-1]
for i in range(1, num_options + 1):
options_dict[i] = similar_words[i - 1][0]
except BaseException:
self.all_sim = []
for word in self.all_words:
if word not in answer:
try:
self.all_sim.append(
(self.model.similarity(answer, word), word))
except BaseException:
self.all_sim.append(
(0.0, word))
else:
self.all_sim.append((-1.0, word))
self.all_sim.sort(reverse=True)
for i in range(1, num_options + 1):
options_dict[i] = self.all_sim[i - 1][1]
replacement_idx = random.randint(1, num_options)
options_dict[replacement_idx] = answer
return options_dict

199
question_extraction.py Normal file
View File

@ -0,0 +1,199 @@
'''This file contains the module for generating
'''
import nltk
import spacy
from nltk.corpus import stopwords
from nltk.tokenize import sent_tokenize, word_tokenize
from sklearn.feature_extraction.text import TfidfVectorizer
class QuestionExtractor:
''' This class contains all the methods
required for extracting questions from
a given document
'''
def __init__(self, num_questions):
self.num_questions = num_questions
# hash set for fast lookup
self.stop_words = set(stopwords.words('english'))
# named entity recognition tagger
self.ner_tagger = spacy.load('en_core_web_md')
self.vectorizer = TfidfVectorizer()
self.questions_dict = dict()
def get_questions_dict(self, document):
'''
Returns a dict of questions in the format:
question_number: {
question: str
answer: str
}
Params:
* document : string
Returns:
* dict
'''
# find candidate keywords
self.candidate_keywords = self.get_candidate_entities(document)
# set word scores before ranking candidate keywords
self.set_tfidf_scores(document)
# rank the keywords using calculated tf idf scores
self.rank_keywords()
# form the questions
self.form_questions()
return self.questions_dict
def get_filtered_sentences(self, document):
''' Returns a list of sentences - each of
which has been cleaned of stopwords.
Params:
* document: a paragraph of sentences
Returns:
* list<str> : list of string
'''
sentences = sent_tokenize(document) # split documents into sentences
return [self.filter_sentence(sentence) for sentence in sentences]
def filter_sentence(self, sentence):
'''Returns the sentence without stopwords
Params:
* sentence: A string
Returns:
* string
'''
words = word_tokenize(sentence)
return ' '.join(w for w in words if w not in self.stop_words)
def get_candidate_entities(self, document):
''' Returns a list of entities according to
spacy's ner tagger. These entities are candidates
for the questions
Params:
* document : string
Returns:
* list<str>
'''
entities = self.ner_tagger(document)
entity_list = []
for ent in entities.ents:
entity_list.append(ent.text)
return list(set(entity_list)) # remove duplicates
def set_tfidf_scores(self, document):
''' Sets the tf-idf scores for each word'''
self.unfiltered_sentences = sent_tokenize(document)
self.filtered_sentences = self.get_filtered_sentences(document)
self.word_score = dict() # (word, score)
# (word, sentence where word score is max)
self.sentence_for_max_word_score = dict()
tf_idf_vector = self.vectorizer.fit_transform(self.filtered_sentences)
feature_names = self.vectorizer.get_feature_names()
tf_idf_matrix = tf_idf_vector.todense().tolist()
num_sentences = len(self.unfiltered_sentences)
num_features = len(feature_names)
for i in range(num_features):
word = feature_names[i]
self.sentence_for_max_word_score[word] = ""
tot = 0.0
cur_max = 0.0
for j in range(num_sentences):
tot += tf_idf_matrix[j][i]
if tf_idf_matrix[j][i] > cur_max:
cur_max = tf_idf_matrix[j][i]
self.sentence_for_max_word_score[word] = self.unfiltered_sentences[j]
# average score for each word
self.word_score[word] = tot / num_sentences
def get_keyword_score(self, keyword):
''' Returns the score for a keyword
Params:
* keyword : string of possible several words
Returns:
* float : score
'''
score = 0.0
for word in word_tokenize(keyword):
if word in self.word_score:
score += self.word_score[word]
return score
def get_corresponding_sentence_for_keyword(self, keyword):
''' Finds and returns a sentence containing
the keywords
'''
words = word_tokenize(keyword)
for word in words:
if word not in self.sentence_for_max_word_score:
continue
sentence = self.sentence_for_max_word_score[word]
all_present = True
for w in words:
if w not in sentence:
all_present = False
if all_present:
return sentence
return ""
def rank_keywords(self):
'''Rank keywords according to their score'''
self.candidate_triples = [] # (score, keyword, corresponding sentence)
for candidate_keyword in self.candidate_keywords:
self.candidate_triples.append([
self.get_keyword_score(candidate_keyword),
candidate_keyword,
self.get_corresponding_sentence_for_keyword(candidate_keyword)
])
self.candidate_triples.sort(reverse=True)
def form_questions(self):
''' Forms the question and populates
the question dict
'''
used_sentences = list()
idx = 0
cntr = 1
num_candidates = len(self.candidate_triples)
while cntr <= self.num_questions and idx < num_candidates:
candidate_triple = self.candidate_triples[idx]
if candidate_triple[2] not in used_sentences:
used_sentences.append(candidate_triple[2])
self.questions_dict[cntr] = {
"question": candidate_triple[2].replace(
candidate_triple[1],
'_' * len(candidate_triple[1])),
"answer": candidate_triple[1]
}
cntr += 1
idx += 1

View File

@ -0,0 +1,56 @@
'''This module ties together the
questions generation and incorrect answer
generation modules
'''
from question_extraction import QuestionExtractor
from incorrect_answer_generation import IncorrectAnswerGenerator
import re
from nltk import sent_tokenize
class QuestionGeneration:
'''This class contains the method
to generate questions
'''
def __init__(self, num_questions, num_options):
self.num_questions = num_questions
self.num_options = num_options
self.question_extractor = QuestionExtractor(num_questions)
def clean_text(self, text):
text = text.replace('\n', ' ') # remove newline chars
sentences = sent_tokenize(text)
cleaned_text = ""
for sentence in sentences:
# remove non alphanumeric chars
cleaned_sentence = re.sub(r'([^\s\w]|_)+', '', sentence)
# substitute multiple spaces with single space
cleaned_sentence = re.sub(' +', ' ', cleaned_sentence)
cleaned_text += cleaned_sentence
if cleaned_text[-1] == ' ':
cleaned_text[-1] = '.'
else:
cleaned_text += '.'
cleaned_text += ' ' # pad with space at end
return cleaned_text
def generate_questions_dict(self, document):
document = self.clean_text(document)
self.questions_dict = self.question_extractor.get_questions_dict(
document)
self.incorrect_answer_generator = IncorrectAnswerGenerator(document)
for i in range(1, self.num_questions + 1):
if i not in self.questions_dict:
continue
self.questions_dict[i]["options"] \
= self.incorrect_answer_generator.get_all_options_dict(
self.questions_dict[i]["answer"],
self.num_options
)
return self.questions_dict

39
requirements.txt Normal file
View File

@ -0,0 +1,39 @@
blis==0.4.1
catalogue==1.0.0
certifi==2020.6.20
chardet==3.0.4
click==7.1.2
cymem==2.0.3
en-core-web-md @ https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.1.tar.gz
en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz
Flask==1.1.2
gensim==3.8.3
idna==2.10
importlib-metadata==2.0.0
itsdangerous==1.1.0
Jinja2==2.11.2
joblib==0.17.0
MarkupSafe==1.1.1
murmurhash==1.0.2
nltk==3.5
numpy==1.19.2
pkg-resources==0.0.0
plac==1.1.3
preshed==3.0.2
PyPDF2==1.26.0
regex==2020.9.27
requests==2.24.0
scikit-learn==0.23.2
scipy==1.5.2
six==1.15.0
sklearn==0.0
smart-open==3.0.0
spacy==2.3.2
srsly==1.0.2
thinc==7.4.1
threadpoolctl==2.1.0
tqdm==4.50.2
urllib3==1.25.10
wasabi==0.8.0
Werkzeug==1.0.1
zipp==3.3.0

105
static/css/quiz.css Normal file
View File

@ -0,0 +1,105 @@
@import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap");
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html,
body {
position: relative;
overflow-x: hidden !important;
}
body {
user-select: none;
margin: 0;
padding: 0;
counter-reset: points;
background-color: #77aa77;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 2 1'%3E%3Cdefs%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='0' x2='0' y1='0' y2='1'%3E%3Cstop offset='0' stop-color='%2377aa77'/%3E%3Cstop offset='1' stop-color='%234fd'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3ClinearGradient id='c' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='2' y2='2'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='0' y='0' fill='url(%23a)' width='2' height='1'/%3E%3Cg fill-opacity='0.5'%3E%3Cpolygon fill='url(%23b)' points='0 1 0 0 2 0'/%3E%3Cpolygon fill='url(%23c)' points='2 1 2 0 0 0'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-size: cover;
font-family: "helvetica", sans-serif !important;
}
a {
text-decoration: none;
color: inherit;
}
section {
padding-top: 150px;
}
main {
-webkit-box-shadow: 0 10px 6px -6px #777;
-moz-box-shadow: 0 10px 6px -6px #777;
box-shadow: 0 10px 6px -6px #777;
color: #000000;
background: #ffffff;
border-radius: 10px;
padding: 50px 40px 50px;
width: 95%;
max-width: 590px;
margin: auto;
}
.text-container {
text-align: center;
}
input[type="radio"] {
display: none;
}
input[type="radio"] + label {
display: inline-block;
width: 100%;
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 10px;
cursor: pointer;
}
input[type="radio"] + label:hover {
border: 1px solid #000000;
}
input[type="radio"]:checked + label {
background-image: none;
background-color: #0c0;
color: #fff;
border: 1px solid #0c0 !important;
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
-ms-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
}
.worngans {
background-color: #f36;
color: #fff;
border: 1px solid #f36 !important;
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
-ms-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
}
.end {
margin: auto;
}
.button.is-fullwidth {
display: flex;
width: 50%;
margin: auto;
margin-top: 30px;
}

57
static/css/results.css Normal file
View File

@ -0,0 +1,57 @@
@import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap");
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html,
body {
position: relative;
overflow-x: hidden !important;
}
body {
user-select: none;
margin: 0;
padding: 0;
counter-reset: points;
background-color: #77aa77;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 2 1'%3E%3Cdefs%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='0' x2='0' y1='0' y2='1'%3E%3Cstop offset='0' stop-color='%2377aa77'/%3E%3Cstop offset='1' stop-color='%234fd'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3ClinearGradient id='c' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='2' y2='2'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='0' y='0' fill='url(%23a)' width='2' height='1'/%3E%3Cg fill-opacity='0.5'%3E%3Cpolygon fill='url(%23b)' points='0 1 0 0 2 0'/%3E%3Cpolygon fill='url(%23c)' points='2 1 2 0 0 0'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-size: cover;
font-family: "helvetica", sans-serif !important;
}
a {
text-decoration: none;
color: inherit;
}
section {
padding-top: 150px;
}
main {
-webkit-box-shadow: 0 10px 6px -6px #777;
-moz-box-shadow: 0 10px 6px -6px #777;
box-shadow: 0 10px 6px -6px #777;
color: #000000;
background: #ffffff;
border-radius: 10px;
padding: 30px 40px 30px;
width: 95%;
max-width: 590px;
margin: auto;
}
.button.is-fullwidth {
display: flex;
width: 90%;
margin: auto;
margin-top: 30px;
}

173
static/css/style.css Normal file
View File

@ -0,0 +1,173 @@
@import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap");
html,
body {
display: flex;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
background-color: #77aa77;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 2 1'%3E%3Cdefs%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='0' x2='0' y1='0' y2='1'%3E%3Cstop offset='0' stop-color='%2377aa77'/%3E%3Cstop offset='1' stop-color='%234fd'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3ClinearGradient id='c' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='2' y2='2'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='0' y='0' fill='url(%23a)' width='2' height='1'/%3E%3Cg fill-opacity='0.5'%3E%3Cpolygon fill='url(%23b)' points='0 1 0 0 2 0'/%3E%3Cpolygon fill='url(%23c)' points='2 1 2 0 0 0'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-size: cover;
font-family: "helvetica", sans-serif !important;
overflow-x: hidden !important;
}
.card {
border-radius: 10px;
margin-top: 50px !important;
height: 350px !important;
width: 250px;
margin: auto;
}
.card.has-text-centered .card-header,
.card.has-text-centered .card-content,
.card.has-text-centered .card-footer {
justify-content: center;
align-items: center;
}
.card.has-text-centered h1 {
font-size: 1.75rem;
font-weight: bold;
}
.navbar {
margin-bottom: 20px !important;
}
.hide {
display: none;
}
.button {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 12.5rem;
margin: 0;
padding: 1.5rem 3.125rem;
background-color: #3498db;
border: none;
border-radius: 0.3125rem;
box-shadow: 0 12px 24px 0 rgba(0, 0, 0, 0.2);
color: white;
font-weight: 400;
overflow: hidden;
font-family: "Open Sans", sans-serif !important;
}
.button:before {
position: absolute;
content: "";
bottom: 0;
left: 0;
width: 0%;
height: 100%;
background-color: #54d98c;
}
.button span {
position: absolute;
line-height: 0;
}
.button span i {
transform-origin: center center;
}
.button span:nth-of-type(1) {
top: 50%;
transform: translateY(-50%);
}
.button span:nth-of-type(2) {
top: 100%;
transform: translateY(0%);
font-size: 24px;
}
.button span:nth-of-type(3) {
display: none;
}
.active {
background-color: #2ecc71;
}
.active:before {
width: 100%;
transition: width 30s linear !important;
}
.active span:nth-of-type(1) {
top: -100%;
transform: translateY(-50%);
}
.active span:nth-of-type(2) {
top: 50%;
transform: translateY(-50%);
}
.active span:nth-of-type(2) i {
animation: loading 10000ms linear infinite !important;
}
.active span:nth-of-type(3) {
display: none;
}
.finished {
background-color: #54d98c;
}
.finished .submit {
display: none;
}
.finished .loading {
display: none;
}
.finished .check {
display: block !important;
font-size: 24px;
animation: scale 0.5s linear;
}
.finished .check i {
transform-origin: center center;
}
@keyframes loading {
100% {
transform: rotate(360deg);
}
}
@keyframes scale {
0% {
transform: scale(10);
}
50% {
transform: scale(0.2);
}
70% {
transform: scale(1.2);
}
90% {
transform: scale(0.7);
}
100% {
transform: scale(1);
}
}
.hero {
margin-top: 70px !important;
}

BIN
static/fonts/helvetica.ttf Normal file

Binary file not shown.

97
templates/index.html Normal file
View File

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='css/style.css') }}" />
<link rel="shortcut icon" href="https://avatars0.githubusercontent.com/u/65834464?s=200&amp;v=4" type="image/x-icon">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
<script src="https://kit.fontawesome.com/22be60108b.js" crossorigin="anonymous"></script>
<title>MLH Quizzet</title>
</head>
<body>
<style type="text/css">
@font-face {
font-family: helvetica;
src: "{{ url_for('static', filename='fonts/helvetica.ttf')}}"
}
</style>
<nav class="navbar is-dark is-fixed-top has-text-centered" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<img src="https://avatars0.githubusercontent.com/u/65834464?s=200&amp;v=4" height="32" width="64"
style="margin: 10px">
<a class="navbar-item has-text-centered" href="{{ url_for('index') }}"><strong
class="is-size-3 has-text-centered">MLH Quizzet</strong></a>
<a class="github-fork-ribbon" href="https://github.com/PragatiVerma18/Fantastic-Falcons-1.0"
data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
<div class="navbar-burger" data-target="navMenu">
<span></span>
<span></span>
<span></span>
</div>
</nav>
<main role="main">
<div class="hero">
<p class="is-size-3 has-text-black has-text-weight-bold has-text-centered">Upload any Document to get an instant Quiz</p>
<p class="is-size-4 has-text-weight-bold has-text-centered">You Know 📖, You Grow 🚀</p>
<p class="is-size-5 has-text-black has-text-weight-medium has-text-centered"> Practice More • Learn More </p>
</div>
<div class="card has-text-centered">
<img src="https://cdn4.iconfinder.com/data/icons/files-and-folders-thinline-icons-set/144/File_PDF-512.png"
alt="upload"/>
<form action="http://localhost:5000/quiz" method="POST" enctype="multipart/form-data">
<div id="file-js-example" class="file has-name is-fullwidth">
<label class="file-label">
<input class="file-input" type="file" name="file" accept=".txt, application/pdf" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label"> Choose a file… </span>
</span>
<span class="file-name"> No file uploaded </span>
</label>
</div>
<script>
const fileInput = document.querySelector(
"#file-js-example input[type=file]"
);
fileInput.onchange = () => {
if (fileInput.files.length > 0) {
const fileName = document.querySelector(
"#file-js-example .file-name"
);
fileName.textContent = fileInput.files[0].name;
}
};
</script>
<button class="button has-text-centered is-large is-fullwidth is-dark">
<span class="submit has-text-centered" type="submit" name="upload file">Submit</span>
<span class="loading"><i class="fa fa-refresh"></i></span>
<span class="check"><i class="fa fa-check"></i></span>
</button>
</form>
</div>
</main>
<script>const button = document.querySelector('.button');
const submit = document.querySelector('.submit');
function toggleClass() {
this.classList.toggle('active');
}
function addClass() {
this.classList.add('finished');
}
button.addEventListener('click', toggleClass);
button.addEventListener('transitionend', toggleClass);
button.addEventListener('transitionend', addClass);
</script>
</body>
</html>

87
templates/quiz.html Normal file
View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='css/quiz.css') }}" />
<link rel="shortcut icon" href="https://avatars0.githubusercontent.com/u/65834464?s=200&amp;v=4"
type="image/x-icon">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
<title>MLH Quizzet</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js" type='text/javascript'></script>
<script type="text/javascript">
$(document).ready(function () {
$('label').click(function () {
$('label').removeClass('worngans');
$(this).addClass('worngans');
});
});
</script>
</head>
<body>
<style type="text/css">
@font-face {
font-family: helvetica;
src: "{{ url_for('static', filename='fonts/helvetica.ttf')}}"
}
</style>
<nav class="navbar is-dark is-fixed-top has-text-centered" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<img src="https://avatars0.githubusercontent.com/u/65834464?s=200&amp;v=4" height="32" width="64"
style="margin: 7px">
<a class="navbar-item has-text-centered" href="{{ url_for('index') }}"><strong
class="is-size-3 has-text-centered">MLH
Quizzet</strong></a>
<a class="github-fork-ribbon" href="https://github.com/PragatiVerma18/Fantastic-Falcons-1.0"
data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
<div class="navbar-burger" data-target="navMenu">
<span></span>
<span></span>
<span></span>
</div>
</div>
</nav>
{% if uploaded == true %}
<form action="http://localhost:5000/result" method="POST">
{% for i in range(size) %}
<section class="section-1" id="section-1">
<main>
<div class="scp-quizzes-main">
<div class="scp-quizzes-data">
<h3 class="is-size-6 has-text-weight-bold">{{ i+1 }}. {{ questions[i+1]['question'] }}</h3>
<br />
{% for op in questions[i+1]['options'] %}
{% if op == questions[i+1]['answer'] %}
<input type="radio" id="{{ questions[i+1]['answer'] }}" name="question{{ i+1 }}">
<label for="{{ questions[i+1]['answer'] }}">
{{ op }}</label><br />
{% else %}
<input type="radio" name="question{{ i+1 }}">
<label> {{ op }}</label><br />
{% endif %}
{% endfor %}
</div>
</div>
</main>
</section>
{% endfor %}
<div class="end">
<button type="submit" class="button is-dark has-text-weight-bold has-text-centered is-fullwidth is-rounded">Submit</button>
</div>
</form>
{% else %}
<section class="section-1" id="section-1">
<h1>Could not upload file</h1>
</section>
{% endif %}
<div class="has-text-white has-text-centered" style="margin-top: 50px; background-color: #363636; padding: 10px;">
MIT License © Copyright 2020 Fantastic Falcons
</div>
</body>
</html>

54
templates/result.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='css/results.css') }}" />
<link rel="shortcut icon" href="https://avatars0.githubusercontent.com/u/65834464?s=200&amp;v=4"
type="image/x-icon">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-fork-ribbon-css/0.2.3/gh-fork-ribbon.min.css" />
<script src="https://kit.fontawesome.com/22be60108b.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>
<title>MLH Quizzet</title>
</head>
<body>
<style type="text/css">
@font-face {
font-family: helvetica;
src: "{{ url_for('static', filename='fonts/helvetica.ttf')}}"
}
</style>
<nav class="navbar is-dark is-fixed-top has-text-centered" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<img src="https://avatars0.githubusercontent.com/u/65834464?s=200&amp;v=4" height="32" width="64"
style="margin: 10px">
<a class="navbar-item has-text-centered" href="{{ url_for('index') }}"><strong
class="is-size-3 has-text-centered">MLH Quizzet</strong></a>
<a class="github-fork-ribbon" href="https://github.com/PragatiVerma18/Fantastic-Falcons-1.0"
data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
<div class="navbar-burger" data-target="navMenu">
<span></span>
<span></span>
<span></span>
</div>
</nav>
<section class="section-1" id="section-1">
<main>
<lottie-player src="https://assets3.lottiefiles.com/packages/lf20_0ge4xP.json" background="transparent" speed="1"
style="width: 240px; height: 240px; margin: auto !important;" loop autoplay></lottie-player>
<h1 class="has-text-centered is-size-3 has-text-weight-bold"> You got {{ correct }}/{{ total }} right!</h1>
<a class="button is-dark has-text-centered has-text-weight-bold is-rounded is-fullwidth" href="{{ url_for('index') }}"> Upload another document</a>
</main>
</section>
<div class="has-text-white has-text-centered" style="margin-top: 40px; background-color: #363636; padding: 10px;">
MIT License © Copyright 2020 Fantastic Falcons
</div>
</body>
</html>

38
workers.py Normal file
View File

@ -0,0 +1,38 @@
from PyPDF2 import PdfFileReader
from question_generation_main import QuestionGeneration
def pdf2text(file_path: str, file_exten: str) -> str:
""" Converts a given file to text content """
_content = ''
# Identify file type and get its contents
if file_exten == 'pdf':
with open(file_path, 'rb') as pdf_file:
_pdf_reader = PdfFileReader(pdf_file)
for p in range(_pdf_reader.numPages):
_content += _pdf_reader.getPage(p).extractText()
# _content = _pdf_reader.getPage(0).extractText()
print('PDF operation done!')
elif file_exten == 'txt':
with open(file_path, 'r') as txt_file:
_content = txt_file.read()
print('TXT operation done!')
return _content
def txt2questions(doc: str, n=5, o=4) -> dict:
""" Get all questions and options """
qGen = QuestionGeneration(n, o)
q = qGen.generate_questions_dict(doc)
for i in range(len(q)):
temp = []
for j in range(len(q[i + 1]['options'])):
temp.append(q[i + 1]['options'][j + 1])
# print(temp)
q[i + 1]['options'] = temp
return q