diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c617d8 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 7ad7ad5..f7e422d 100644 --- a/README.md +++ b/README.md @@ -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. @@ -21,7 +21,8 @@ This is a smart Quiz Generator that generates a dynamic quiz from any uploaded t ## Technology Stack: - + + - **Frontend**: HTML, CSS, Vanilla JS - **Backend**: Flask @@ -107,6 +108,12 @@ $ python app.py | 2. | Kshitij Kotasthane | Backend Developer | [@kshitij86](https://github.com/kshitij86) | | 3. | Vignesh S | ML | [@telescopic](https://github.com/telescopic) | + + + + + + ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/app.py b/app.py new file mode 100644 index 0000000..81d8b5a --- /dev/null +++ b/app.py @@ -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) diff --git a/incorrect_answer_generation.py b/incorrect_answer_generation.py new file mode 100644 index 0000000..149dd43 --- /dev/null +++ b/incorrect_answer_generation.py @@ -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 diff --git a/question_extraction.py b/question_extraction.py new file mode 100644 index 0000000..84d8d82 --- /dev/null +++ b/question_extraction.py @@ -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 : 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 + ''' + 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 diff --git a/question_generation_main.py b/question_generation_main.py new file mode 100644 index 0000000..7d66d1c --- /dev/null +++ b/question_generation_main.py @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5141dfa --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/static/css/quiz.css b/static/css/quiz.css new file mode 100644 index 0000000..71698f4 --- /dev/null +++ b/static/css/quiz.css @@ -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; +} diff --git a/static/css/results.css b/static/css/results.css new file mode 100644 index 0000000..7f46108 --- /dev/null +++ b/static/css/results.css @@ -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; +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..cd8d4f8 --- /dev/null +++ b/static/css/style.css @@ -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; +} diff --git a/static/fonts/helvetica.ttf b/static/fonts/helvetica.ttf new file mode 100644 index 0000000..4d99761 Binary files /dev/null and b/static/fonts/helvetica.ttf differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..efb0c1f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,97 @@ + + + + + + + + + + + + MLH Quizzet + + + + + + + + MLH Quizzet + Fork me on GitHub + + + + + + + + + Upload any Document to get an instant Quiz + You Know 📖, You Grow 🚀 + Practice More • Learn More + + + + + + + + + + + + Choose a file… + + No file uploaded + + + + + Submit + + + + + + + + + + \ No newline at end of file diff --git a/templates/quiz.html b/templates/quiz.html new file mode 100644 index 0000000..faa8a08 --- /dev/null +++ b/templates/quiz.html @@ -0,0 +1,87 @@ + + + + + + + + + + + MLH Quizzet + + + + + + + + + + MLH + Quizzet + Fork me on GitHub + + + + + + + + {% if uploaded == true %} + + {% for i in range(size) %} + + + + + {{ i+1 }}. {{ questions[i+1]['question'] }} + + {% for op in questions[i+1]['options'] %} + {% if op == questions[i+1]['answer'] %} + + + {{ op }} + {% else %} + + {{ op }} + {% endif %} + {% endfor %} + + + + + {% endfor %} + + Submit + + + + {% else %} + + Could not upload file + + {% endif %} + + MIT License © Copyright 2020 Fantastic Falcons + + + + \ No newline at end of file diff --git a/templates/result.html b/templates/result.html new file mode 100644 index 0000000..e7f6831 --- /dev/null +++ b/templates/result.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + MLH Quizzet + + + + + + + + MLH Quizzet + Fork me on GitHub + + + + + + + + + + You got {{ correct }}/{{ total }} right! + Upload another document + + + + + + MIT License © Copyright 2020 Fantastic Falcons + + + + \ No newline at end of file diff --git a/workers.py b/workers.py new file mode 100644 index 0000000..953b32a --- /dev/null +++ b/workers.py @@ -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
Upload any Document to get an instant Quiz
You Know 📖, You Grow 🚀
Practice More • Learn More