Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """
- A quiz program made under Python to allow testing of student's knowledge over
- several subject such as:
- Computer Science
- History
- Music
- et cetera.
- """
- import json # json.loads, json.decoder.JSONDecodeError, json.dumps
- import sys # sys.exit, sys.argv
- import hashlib # hashlib.sha1
- import base64 # base64.b64encode
- import math # math.ceil
- import time # time.sleep
- import itertools # itertools.cycle
- import os # os.path.exists
- # the above code imports libraries so i can use different tools reinventing the wheel isn't good practice because master shoaib wants the code to be more efficien
- def poll_input(question, sequence, map_fn=str, call_lower=False, catch=Exception, catch_caller=sys.exit):
- try:
- resp = input(question)
- if resp:
- resp = map_fn(resp)
- except catch as exc:
- if callable(catch_caller):
- catch_caller(exc)
- if call_lower:
- resp = resp.lower() # type must be of basestring
- while resp not in sequence:
- try:
- _resp = input(question)
- if _resp:
- _resp = map_fn(_resp)
- except catch as exc:
- if callable(catch_caller):
- catch_caller(exc)
- if call_lower:
- _resp = _resp.lower()
- resp = _resp
- return resp
- def register(*args):
- print("Login Requirements:\n\t- Name with 3 <= length <= 64\n\t- Age\n\t- Year group\n\t- Password of 4 < length < 64")
- name = input("{?} Name: ")
- if not 3 <= len(name) <= 64:
- print("{*} Name must be within the 3-64 character range.")
- return False
- try:
- age = int(input("{?} Age: "))
- except ValueError:
- print("{*} Age invalid.")
- return False
- year_group = input("{?} Year group: ")
- password = input("{?} Password: ")
- if not 4 < len(password) < 64:
- print("{*} Password must be more than 4 characters but less than 64.")
- return False
- username = "%s%s" % (name[:3], age)
- if username in args[1]:
- print("{*} Username '%s' already exists." % username)
- return False
- password = base64.b64encode(hashlib.sha1(password.encode()).digest()).decode()
- args[1][username] = (name, age, year_group, password)
- with open(args[0], 'w') as dbs_write:
- dbs_write.write(json.dumps(args[1]))
- return (name, age, year_group, password)
- def round_nearest_ten(n):
- return int(math.ceil(n/10.0))*10
- def save_file(data, filename):
- with open(filename, 'w') as save_data:
- save_data.write(json.dumps(data))
- def login(*args):
- args = list(args)
- name = input("{?} Name: ")
- try:
- age = int(input("{?} Age: "))
- except ValueError:
- print("{*} Age invalid.")
- return False
- username = "%s%s" % (name[:3], age)
- password = input("{?} Password: ")
- password = base64.b64encode(hashlib.sha1(password.encode()).digest()).decode()
- # this bit first turns the password into a raw format (binary), then hashes the password into sha1 then base64 encoded it and decodes
- # it from binary (you could say this is unnecessary, but it provides added security)
- # it's a scalable system which allows for friendlier user input for subjects and questions by the config.ini file which allows you to
- # input more subjects and add varied difficulties for them and easily integrate them without much pain
- # then the person can add the subjects in a pretty trivial fashion by making a csv-formatted file which is linked to by the config.ini, # then beyond that everything should work for a typical user
- # the registration/login system is pretty normal as the login system doesn't ever know the actual password for the user being logged
- # into, only the hash, so the program hashes the password given, then compares it to the hash that the database.dbs file has, if
- # they're #the same, then the user entered the correct password and if not, then they're different
- # this makes it harder to break as the user would need to crack the hash which is harder as no aspects are given to base a bruteforce off
- # the only issue really presented is the changeability in the files, e.g. the user could change the hashes around and it'll work just
- # fine, however this can be easily prevented by a trivial form of encryption
- if username not in args[1]:
- resp = poll_input("{*} Username not found in cached database, should I reload my database (y[es]/n[o])? ",
- ["yes", "y", "no", "n"],
- call_lower=True
- )
- if 'y' in resp:
- with open(args[0]) as update:
- args[1] = json.loads(update.read())
- if username not in (name[0] for name in args[1]):
- print("{*} Username not found in cached nor new database, please register.")
- return False
- print("{*} Found user in update database.")
- elif 'n' in resp:
- return False
- if base64.b64decode(args[1][username][3]) != base64.b64decode(password):
- print("{*} Incorrect password.")
- return False
- return (args[1][username][0], args[1][username][1], args[1][username][2], args[1][username][3])
- config_file = "config.ini"
- dbs_file = "database.dbs"
- results_file = "results.file"
- if len(sys.argv) >= 2:
- config_file = sys.argv[1]
- if len(sys.argv) >= 3:
- dbs_file = sys.argv[2]
- if len(sys.argv) == 4:
- results_file = sys.argv[3]
- #
- ## Add your own subject into the config.ini
- ## with the following template
- ## "Subject Name": {
- ## "Hard": "path/to/file.csv",
- ## "Medium": "path/to/file.csv",
- ## "Easy": "path/to/file.csv",
- ## }
- #
- ## Assure that all of the contents are enclo
- ## sed inside {...} so that it can be read
- ## safely by the computer without causing
- ## errors
- #
- try:
- with open(config_file) as config:
- try:
- questions = json.loads(config.read())
- except json.decoder.JSONDecodeError:
- print("{*} Your '%s' file has syntax errors in it!" % config_file)
- sys.exit(1)
- except FileNotFoundError:
- print("{*} '%s' doesn't exist, please create it." % config_file)
- sys.exit(1)
- current_user = []
- users = {}
- fn_map = {
- "register": register,
- "login": login
- }
- grade_map = {
- 0: 'U',
- 10: 'F',
- 20: 'F',
- 30: 'F',
- 40: 'E',
- 50: 'D',
- 60: 'C',
- 70: 'B',
- 80: 'A',
- 90: 'A',
- 100: 'A*'
- }
- try:
- with open(dbs_file) as database:
- try:
- users = json.loads(database.read())
- except json.decoder.JSONDecodeError:
- print("{*} Your '%s' file has syntax errors in it!" % dbs_file)
- sys.exit(1)
- except FileNotFoundError:
- with open(dbs_file, 'w') as dbs:
- dbs.write("{}") # the natural json.loads(...) breaks without this
- resp = poll_input("{?} Would you like to register or login? ", ["register", "login"], call_lower=True)
- data = fn_map[resp](dbs_file, users)
- if not data:
- print("{*} Failed to %s." % resp)
- sys.exit(1)
- print("{*} Successfully registered/logged into %s." % data[0]) # note: registering an account automatically logs you in
- current_user.extend(data)
- current_user.append({
- k: {
- v_k: [0, 0] for v_k in v
- } for k, v in questions.items()
- }
- )
- # the above code looks a bit ugly however it simply creates a 'topic: {for topic difficulty in topic: (0_0, 0_1)}'
- # where 0_0 is the current test-score and 0_1 is the total questions answered
- if os.path.exists(results_file): # bad practice
- resp = poll_input("{!} There is an existing '%s' file, would you like to load it (y[es]/n[o])? " % (
- results_file
- ),
- ["yes", 'y', "no", 'n'],
- call_lower=True)
- if 'y' in resp:
- with open(results_file) as read_data:
- temp_data = json.loads(read_data.read())
- if temp_data[:-1] != current_user[:-1]:
- print("{*} Loading '%s' file with non-matching data, exiting..." % results_file)
- sys.exit(1)
- current_user = temp_data
- chosen_subject = []
- while 1:
- print("Here are your quiz scores: ")
- for topic, v in current_user[-1].items():
- for diff, scores in v.items():
- print("%s-%s:\n\t%s%% for %s out of %s questions, thus achieving a(n) %s grade." % (
- diff,
- topic,
- int(100/scores[1])*scores[0] if scores[1] else 0,
- scores[0],
- scores[1],
- grade_map[round_nearest_ten(int(100/scores[1])*scores[0] if scores[1] else 0)])
- )
- print()
- for topic, v in itertools.cycle(questions.items()):
- if chosen_subject:
- break
- # it basically makes an infinite for loop which loops over questions.items()'s key and value pairs over and over
- # so a = itertools.cycle([1, 2])
- resp = poll_input("{?} Would you like to take %s (y[es]/n[o])? " % topic, ["yes", 'y', "no", 'n', "quit", 'q'], call_lower=True)
- if 'n' in resp:
- continue
- elif 'q' in resp:
- print("{*} Saving and exiting...")
- save_file(current_user, results_file)
- sys.exit(0)
- for diff, file_ in v.items():
- resp = poll_input("{?} Would you like to take the %s level (y[es]/n[o])? " % diff, ["yes", 'y', "no", 'n', "quit", 'q'], call_lower=True)
- if 'n' in resp:
- continue
- elif 'q' in resp:
- print("{*} Saving and exiting...")
- save_file(current_user, results_file)
- sys.exit(0)
- chosen_subject = [topic, diff]
- file_path = questions[topic][diff]
- break
- with open(file_path) as quiz:
- quiz = quiz.readlines()
- current_user[-1][chosen_subject[0]][chosen_subject[1]][1] += len(quiz)
- for question in quiz:
- qst, *options, correct = question.strip().rsplit(',', 5)
- resp = poll_input("Question: '%s'\n%s\nSelect by index: " % (qst, '\n'.join("%s) %s" % (i+1, v) for i, v in enumerate(options))),
- range(1, len(options)+1),
- map_fn=int,
- catch=(TypeError, ValueError))
- if not resp:
- print("{*} Invalid index chosen!")
- continue
- # poll_input is a functino that basically makes sure that a user inputs the correct thing and keeps polling them (keeps making them re-# enter their input) if they don't enter something within a sequence of items, so in this case, if the user doesn't input any number
- # between range(1, len(options)+1) (how many options there are for the question), ask them to reenter it until they enter a valid
- # choice
- # map_fn=int, tells the function to make every input the user takes in an integer, whereas normally it's a string, as range
- # (1,len(options)+1) returns a list of integers and not strings, so you can't do "1" in [1, 2, 3] as you're comparing a string "1" to a
- # list full of integers [1, 2, 3]
- # catch=(TypeError, ValueError) tells the function what sorts of errors it should catch in the case that the given map_fn fails, so say # the user enters "a", which can't be converted to an integer, it would raise a ValueError which will be caught as specified in the
- # arguments, and then an implicit function will be called to handle the exception.
- # "Question: '%s'\n%s\nSelect by index: " % (qst, '\n'.join("%s) %s" % (i+1, v) for i, v in enumerate(options))
- # this itself just goes over each question and its corresponding index and formats it like
- # Question: 'this is the question'
- # 1) choice 1
- # 2) choice 2
- # 3) choice 3
- # Select by index: <input-here>
- # and the user would enter a number between 1 to 3
- # if it's any lower or bigger, it'll keep asking until it gets the required input
- # enumerate(list) simply returns an (index, value)-ified list
- # e.g.
- # lst = ["this", "that", "there"]
- # lst = enumerate(lst)
- # print(lst)
- # >>> [(0, "this"), (1, "that"), (2, "there")]
- correct = int(correct)+1
- if resp == correct:
- print("{!} You got the question correct!")
- current_user[-1][chosen_subject[0]][chosen_subject[1]][0] += 1
- time.sleep(0.5)
- else:
- print("{!} Your answer was incorrect.")
- time.sleep(0.5)
- save_file(current_user, results_file)
- chosen_subject = []
Add Comment
Please, Sign In to add comment