diff --git a/.gitignore b/.gitignore index 3b01ea3..7886722 100644 --- a/.gitignore +++ b/.gitignore @@ -64,10 +64,8 @@ target/ .idea/ venv/ - +.vscode/ # Folder .pytest_cache -submission_record.db -user_record.db -session_record.db \ No newline at end of file +data.db \ No newline at end of file diff --git a/Pipfile b/Pipfile index 67ada83..d7b51a6 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ bottle = "*" pip = "==18.0" pre-commit = "*" black = "*" +peewee = "*" [dev-packages] requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index c853808..464d309 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "609955005164d92d175b8f2b4f17a281870e7b835b9267bc3e9b79ebaef15356" + "sha256": "c2d78e0bf5fe824233f66af401871c1cab11b264303260a17b277bac99bddaa6" }, "pipfile-spec": 6, "requires": { @@ -69,17 +69,17 @@ }, "identify": { "hashes": [ - "sha256:244e7864ef59f0c7c50c6db73f58564151d91345cd9b76ed793458953578cadd", - "sha256:8ff062f90ad4b09cfe79b5dfb7a12e40f19d2e68a5c9598a49be45f16aba7171" + "sha256:432c548d6138cb57a3d8f62f079a025a29b8ae34a50dd3b496bbf661818f2bc0", + "sha256:d4401d60bf1938aa3074a352a5cc9044107edf11a6fedd3a1db172c141619b81" ], - "version": "==1.4.1" + "version": "==1.4.3" }, "importlib-metadata": { "hashes": [ - "sha256:46fc60c34b6ed7547e2a723fc8de6dc2e3a1173f8423246b3ce497f064e9c3de", - "sha256:bc136180e961875af88b1ab85b4009f4f1278f8396a60526c0009f503a1a96ca" + "sha256:2f2e54cbf6b06b16351e4c40a6adb0860cab6cfb95a0c0fcb58bb789c4b450f5", + "sha256:37bbea81dec44d1ff72d58a1b5c1599a9f3436537f33e9e26f276610064c4830" ], - "version": "==0.9" + "version": "==0.12" }, "importlib-resources": { "hashes": [ @@ -95,13 +95,20 @@ ], "version": "==1.3.3" }, + "peewee": { + "hashes": [ + "sha256:f0249be468e3b119a8ad83f686e7fe161303197e0534e3cdff8fa5a5417c01a5" + ], + "index": "pypi", + "version": "==3.9.5" + }, "pre-commit": { "hashes": [ - "sha256:75a9110eae00d009c913616c0fc8a6a02e7716c4a29a14cac9b313d2c7338ab0", - "sha256:f882c65316eb5b705fe4613e92a7c91055c1800102e4d291cfd18912ec9cf90e" + "sha256:6ca409d1f22d444af427fb023a33ca8b69625d508a50e1b7eaabd59247c93043", + "sha256:94dd519597f5bff06a4b0df194a79c524b78f4b1534c1ce63241a9d4fb23b926" ], "index": "pypi", - "version": "==1.15.1" + "version": "==1.16.1" }, "pyyaml": { "hashes": [ @@ -135,17 +142,17 @@ }, "virtualenv": { "hashes": [ - "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", - "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + "sha256:99acaf1e35c7ccf9763db9ba2accbca2f4254d61d1912c5ee364f9cc4a8942a0", + "sha256:fe51cdbf04e5d8152af06c075404745a7419de27495a83f0d72518ad50be3ce8" ], - "version": "==16.4.3" + "version": "==16.6.0" }, "zipp": { "hashes": [ - "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478", - "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4" + "sha256:46dfd547d9ccbf8bdc26ecea52818046bb28509f12bb6a0de1cd66ab06e9a9be", + "sha256:d7ac25f895fb65bff937b381353c14eb1fa23d35f40abd72a5342cd57eb57fd1" ], - "version": "==0.3.3" + "version": "==0.5.0" } }, "develop": { @@ -194,10 +201,10 @@ }, "pluggy": { "hashes": [ - "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", - "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + "sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180", + "sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a" ], - "version": "==0.9.0" + "version": "==0.11.0" }, "py": { "hashes": [ @@ -208,11 +215,11 @@ }, "pytest": { "hashes": [ - "sha256:13c5e9fb5ec5179995e9357111ab089af350d788cbc944c628f3cde72285809b", - "sha256:f21d2f1fb8200830dcbb5d8ec466a9c9120e20d8b53c7585d180125cce1d297a" + "sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24", + "sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.5.0" }, "requests": { "hashes": [ @@ -231,10 +238,17 @@ }, "urllib3": { "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + ], + "version": "==1.24.3" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" ], - "version": "==1.24.1" + "version": "==0.1.7" } } } diff --git a/server.py b/server.py index 2c20ab1..f1f874d 100644 --- a/server.py +++ b/server.py @@ -1,60 +1,122 @@ import bottle import os, sys, datetime import string, random - -from collections import defaultdict, namedtuple -import shelve +from peewee import * path = os.path.abspath(__file__) dir_path = os.path.dirname(path) app = bottle.Bottle() -database_path = "submission_record.db" -user_db = "user_record.db" -sessions_db = "session_record.db" -questions = {} -contests = {} +DATABASE_NAME = "data.db" question_dir = "files/questions" -Question = namedtuple("Question", "output statement") -Submission = namedtuple("Submission", "question time output is_correct contest") -Contest = namedtuple("Contest", "description questions start_time end_time") -User = namedtuple("User", "password") +db = SqliteDatabase(DATABASE_NAME) + + +class User(Model): + username = CharField(unique=True) + password = CharField() + + class Meta: + database = db + + +class Session(Model): + def random_token(): + return "".join([random.choice(string.ascii_letters) for _ in range(20)]) + + token = CharField(unique=True, default=random_token) + user = ForeignKeyField(User) + + class Meta: + database = db + + +class Contest(Model): + code = CharField(unique=True) + description = CharField() + start_time = DateTimeField() + end_time = DateTimeField() + + class Meta: + database = db + + +class Question(Model): + q_no = IntegerField(unique=True) + author = ForeignKeyField(User) + + class Meta: + database = db + + +class ContestProblems(Model): + contest = ForeignKeyField(Contest, backref="questions") + question = ForeignKeyField(Question) + + class Meta: + database = db + indexes = ((("contest", "question"), True),) + + +class Submission(Model): + user = ForeignKeyField(User) + time = DateTimeField() + contestProblem = ForeignKeyField(ContestProblems) + is_correct = BooleanField() + + class Meta: + database = db + indexes = ((("user", "time"), True),) + + +db.connect() +db.create_tables([User, Session, Submission, ContestProblems, Contest, Question]) -# dummy contests -contests["PRACTICE"] = Contest( + +# dummy contest data +practiceContest = Contest.get_or_create( + code="PRACTICE", description="practice questions", - questions=[1, 2], start_time=datetime.datetime(day=1, month=1, year=1), end_time=datetime.datetime(day=1, month=1, year=9999), ) -contests["PASTCONTEST"] = Contest( +pastContest = Contest.get_or_create( + code="PASTCONTEST", description="somewhere in the past", - questions=[1, 2], start_time=datetime.datetime(day=1, month=11, year=2018), end_time=datetime.datetime(day=1, month=12, year=2018), ) -contests["ONGOINGCONTEST"] = Contest( +ongoingContest = Contest.get_or_create( + code="ONGOINGCONTEST", description="somewhere in the present", - questions=[3, 4], start_time=datetime.datetime(day=1, month=4, year=2019), end_time=datetime.datetime(day=1, month=6, year=2019), ) -contests["FUTURECONTEST"] = Contest( +futureContest = Contest.get_or_create( + code="FUTURECONTEST", description="somewhere in the future", - questions=[5, 6], start_time=datetime.datetime(day=1, month=1, year=2020), end_time=datetime.datetime(day=1, month=10, year=2020), ) -for i in os.listdir(question_dir): - if not i.isdigit(): - continue - with open(os.path.join(question_dir, i, "output.txt"), "rb") as fl: - output = fl.read() - with open(os.path.join(question_dir, i, "statement.txt"), "r") as fl: - statement = fl.read() - questions[i] = Question(output=output, statement=statement) +test = User.get_or_create(username="test", password="test") + +q1 = Question.get_or_create(q_no=1, author=test[0]) +q2 = Question.get_or_create(q_no=2, author=test[0]) +q3 = Question.get_or_create(q_no=3, author=test[0]) +q4 = Question.get_or_create(q_no=4, author=test[0]) +q5 = Question.get_or_create(q_no=5, author=test[0]) +q6 = Question.get_or_create(q_no=6, author=test[0]) + +ContestProblems.get_or_create(contest=practiceContest[0], question=q1[0]) +ContestProblems.get_or_create(contest=practiceContest[0], question=q2[0]) +ContestProblems.get_or_create(contest=pastContest[0], question=q1[0]) +ContestProblems.get_or_create(contest=pastContest[0], question=q2[0]) +ContestProblems.get_or_create(contest=ongoingContest[0], question=q3[0]) +ContestProblems.get_or_create(contest=ongoingContest[0], question=q4[0]) +ContestProblems.get_or_create(contest=futureContest[0], question=q5[0]) +ContestProblems.get_or_create(contest=futureContest[0], question=q6[0]) def login_required(function): @@ -62,8 +124,10 @@ def login_redirect(*args, **kwargs): if not logggedIn(): return bottle.template("home.html", message="Login required.") return function(*args, **kwargs) + return login_redirect + @app.route("/") def changePath(): return bottle.redirect("/home") @@ -79,17 +143,26 @@ def home(): @app.get("/dashboard") @login_required def dashboard(): + contests = Contest.select().order_by(Contest.start_time) return bottle.template("dashboard.html", contests=contests) @app.get("/contest//") @login_required -def contest(code, number): - if not code in contests: - return "Contest does not exist" - if contests[code].start_time > datetime.datetime.now(): +def question(code, number): + if ( + not ContestProblems.select() + .where((Contest.code == code) & (Question.q_no == int(number))) + .join(Contest, on=(ContestProblems.contest == Contest.id)) + .join(Question, on=(ContestProblems.question == Question.q_no)) + .exists() + ): + return bottle.abort(404, "no such contest problem") + contest = Contest.get(Contest.code == code) + if contest.start_time > datetime.datetime.now(): return "The contest had not started yet." - statement = questions[number].statement + with open(os.path.join(question_dir, number, "statement.txt"), "rb") as fl: + statement = fl.read() return bottle.template( "question.html", question_number=number, contest=code, question=statement ) @@ -98,11 +171,13 @@ def contest(code, number): @app.get("/contest/") @login_required def contest(code): - if not code in contests: - return "Contest does not exist" - if contests[code].start_time > datetime.datetime.now(): + try: + contest = Contest.get(Contest.code == code) + except Contest.DoesNotExist: + return bottle.abort(404, "no such contest") + if contest.start_time > datetime.datetime.now(): return "The contest had not started yet." - return bottle.template("contest.html", code=code, contest=contests[code]) + return bottle.template("contest.html", contest=contest, questions=contest.questions) @app.get("/question/") @@ -117,74 +192,66 @@ def server_static(filepath): @app.get("/ranking/") def contest_ranking(code): - with shelve.open(database_path) as submission_record: - order = [ - ( - user, - len( - set( - [ - attempt.question - for attempt in submissions - if ( - attempt.is_correct - and (int(attempt.question) in contests[code].questions) - and attempt.contest == code - and attempt.time <= contests[code].end_time - and attempt.time >= contests[code].start_time - ) - ] - ) - ), - ) - for user, submissions in submission_record.items() - ] - order.sort(key=lambda x: x[1], reverse=True) - order = [entry for entry in order if entry[1] > 0] - order = [(user, score, rank) for rank, (user, score) in enumerate(order, start=1)] + order = ( + Submission.select( + User.username, fn.count(Submission.contestProblem.distinct()).alias("score") + ) + .where( + (Submission.is_correct == True) + & (ContestProblems.contest == Contest.get(Contest.code == code)) + ) + .join(User, on=(Submission.user == User.id)) + .switch() + .join(ContestProblems, on=(Submission.contestProblem == ContestProblems.id)) + .group_by(Submission.user) + .order_by(fn.count(Submission.contestProblem.distinct()).desc()) + ) + order = list(order.tuples()) + order = [ + (username, score, rank) for rank, (username, score) in enumerate(order, start=1) + ] return bottle.template("rankings.html", people=order) @app.get("/ranking") def rankings(): - with shelve.open(database_path) as submission_record: - order = [ - ( - user, - len( - set( - [ - attempt.question - for attempt in submissions - if attempt.is_correct - ] - ) - ), - ) - for user, submissions in submission_record.items() - ] - order.sort(key=lambda x: x[1], reverse=True) - order = [(user, score, rank) for rank, (user, score) in enumerate(order, start=1)] - return template("rankings.html", people=order) + order = ( + Submission.select( + User.username, fn.count(Submission.contestProblem.distinct()).alias("score") + ) + .where((Submission.is_correct == True)) + .join(User, on=(Submission.user == User.id)) + .switch() + .join(ContestProblems, on=(Submission.contestProblem == ContestProblems.id)) + .group_by(Submission.user) + .order_by(fn.count(Submission.contestProblem.distinct()).desc()) + ) + order = list(order.tuples()) + order = [ + (username, score, rank) for rank, (username, score) in enumerate(order, start=1) + ] + return bottle.template("rankings.html", people=order) def logggedIn(): if not bottle.request.get_cookie("s_id"): return False - with shelve.open(sessions_db) as sessions: - return bottle.request.get_cookie("s_id") in sessions + return ( + Session.select() + .where(Session.token == bottle.request.get_cookie("s_id")) + .exists() + ) def createSession(username): - session_id = "".join( - random.choice(string.ascii_letters + string.digits) for i in range(20) - ) + try: + session = Session.create(user=User.get(User.username == username)) + except IntegrityError: + return bottle.abort(500, "Error! Please try again.") bottle.response.set_cookie( "s_id", - session_id, + session.token, expires=datetime.datetime.now() + datetime.timedelta(days=30), ) - with shelve.open(sessions_db) as sessions: - sessions[session_id] = username return bottle.redirect("/dashboard") @@ -192,11 +259,12 @@ def createSession(username): def login(): username = bottle.request.forms.get("username") password = bottle.request.forms.get("password") - with shelve.open(user_db) as users: - if not username in users: - return bottle.template("home.html", message="User does not exist.") - if users[username].password != password: - return bottle.template("home.html", message="Incorrect password.") + if ( + not User.select() + .where((User.username == username) & (User.password == password)) + .exists() + ): + return bottle.template("home.html", message="Invalid credentials.") return createSession(username) @@ -204,20 +272,18 @@ def login(): def register(): username = bottle.request.forms.get("username") password = bottle.request.forms.get("password") - with shelve.open(user_db) as users: - if username in users: - return bottle.template( - "home.html", - message="Username already exists. Select a different username", - ) - users[username] = User(password=password) + try: + User.create(username=username, password=password) + except IntegrityError: + return bottle.template( + "home.html", message="Username already exists. Select a different username" + ) return createSession(username) @app.get("/logout") def logout(): - with shelve.open(sessions_db) as sessions: - del sessions[bottle.request.get_cookie("s_id")] + Session.delete().where(Session.token == bottle.request.get_cookie("s_id")).execute() bottle.response.delete_cookie("s_id") return bottle.redirect("/home") @@ -225,30 +291,27 @@ def logout(): @app.post("/check//") @login_required def file_upload(code, number): - with shelve.open(sessions_db) as sessions: - u_name = sessions[bottle.request.get_cookie("s_id")] + try: + contestProblem = ContestProblems.get( + ContestProblems.contest == Contest.get(Contest.code == code), + ContestProblems.question == Question.get(Question.q_no == int(number)), + ) + except: + return bottle.abort(404, "no such contest problem") + user = Session.get(Session.token == bottle.request.get_cookie("s_id")).user time = datetime.datetime.now() uploaded = bottle.request.files.get("upload").file.read() - expected = questions[number].output + with open(os.path.join(question_dir, number, "output.txt"), "rb") as fl: + expected = fl.read() expected = expected.strip() uploaded = uploaded.strip() ans = uploaded == expected - - with shelve.open(database_path) as submission_record: - submissions = ( - [] if u_name not in submission_record else submission_record[u_name] - ) - submissions.append( - Submission( - question=number, - time=time, - output=uploaded, - is_correct=ans, - contest=code, - ) + try: + Submission.create( + user=user, contestProblem=contestProblem, time=time, is_correct=ans ) - submission_record[u_name] = submissions - + except: + bottle.abort(500, "Error in inserting submission to database.") if not ans: return "Wrong Answer!!" else: @@ -257,6 +320,7 @@ def file_upload(code, number): @app.error(404) def error404(error): - return template("error.html" ,errorcode=error.status_code , errorbody = error.body) + return template("error.html", errorcode=error.status_code, errorbody=error.body) + -bottle.run(app, host="localhost", port=8080) \ No newline at end of file +bottle.run(app, host="localhost", port=8080) diff --git a/views/contest.html b/views/contest.html index c7a3f12..ed2e653 100644 --- a/views/contest.html +++ b/views/contest.html @@ -2,7 +2,7 @@
-

{{code}}

+

{{contest.code}}

@@ -10,11 +10,11 @@

{{code}}

- % for qno in range(len(contest.questions)): - {{qno+1}}
+ % for qno in range(len(questions)): + {{qno+1}}
% end
diff --git a/views/dashboard.html b/views/dashboard.html index 1349b5c..91974be 100644 --- a/views/dashboard.html +++ b/views/dashboard.html @@ -13,9 +13,9 @@

Contests

- % for code, contest in contests.items(): + % for contest in contests: - {{code}} + {{contest.code}} {{contest.start_time}} {{contest.end_time}}