Guest User

Untitled

a guest
Oct 29th, 2017
39
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.61 KB | None | 0 0
  1. """
  2. A quiz program made under Python to allow testing of student's knowledge over
  3. several subject such as:
  4. Computer Science
  5. History
  6. Music
  7. et cetera.
  8. """
  9.  
  10. import json # json.loads, json.decoder.JSONDecodeError, json.dumps
  11. import sys # sys.exit, sys.argv
  12. import hashlib # hashlib.sha1
  13. import base64 # base64.b64encode
  14. import math # math.ceil
  15. import time # time.sleep
  16. import itertools # itertools.cycle
  17. import os # os.path.exists
  18. # 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
  19.  
  20. def poll_input(question, sequence, map_fn=str, call_lower=False, catch=Exception, catch_caller=sys.exit):
  21. try:
  22. resp = input(question)
  23. if resp:
  24. resp = map_fn(resp)
  25. except catch as exc:
  26. if callable(catch_caller):
  27. catch_caller(exc)
  28.  
  29. if call_lower:
  30. resp = resp.lower() # type must be of basestring
  31.  
  32. while resp not in sequence:
  33. try:
  34. _resp = input(question)
  35. if _resp:
  36. _resp = map_fn(_resp)
  37. except catch as exc:
  38. if callable(catch_caller):
  39. catch_caller(exc)
  40.  
  41. if call_lower:
  42. _resp = _resp.lower()
  43.  
  44. resp = _resp
  45.  
  46. return resp
  47.  
  48.  
  49. def register(*args):
  50. print("Login Requirements:\n\t- Name with 3 <= length <= 64\n\t- Age\n\t- Year group\n\t- Password of 4 < length < 64")
  51.  
  52. name = input("{?} Name: ")
  53.  
  54. if not 3 <= len(name) <= 64:
  55. print("{*} Name must be within the 3-64 character range.")
  56. return False
  57.  
  58. try:
  59. age = int(input("{?} Age: "))
  60. except ValueError:
  61. print("{*} Age invalid.")
  62. return False
  63.  
  64. year_group = input("{?} Year group: ")
  65. password = input("{?} Password: ")
  66.  
  67. if not 4 < len(password) < 64:
  68. print("{*} Password must be more than 4 characters but less than 64.")
  69. return False
  70.  
  71. username = "%s%s" % (name[:3], age)
  72.  
  73. if username in args[1]:
  74. print("{*} Username '%s' already exists." % username)
  75. return False
  76.  
  77. password = base64.b64encode(hashlib.sha1(password.encode()).digest()).decode()
  78. args[1][username] = (name, age, year_group, password)
  79.  
  80. with open(args[0], 'w') as dbs_write:
  81. dbs_write.write(json.dumps(args[1]))
  82.  
  83. return (name, age, year_group, password)
  84.  
  85.  
  86. def round_nearest_ten(n):
  87. return int(math.ceil(n/10.0))*10
  88.  
  89.  
  90. def save_file(data, filename):
  91. with open(filename, 'w') as save_data:
  92. save_data.write(json.dumps(data))
  93.  
  94.  
  95. def login(*args):
  96. args = list(args)
  97. name = input("{?} Name: ")
  98.  
  99. try:
  100. age = int(input("{?} Age: "))
  101. except ValueError:
  102. print("{*} Age invalid.")
  103. return False
  104.  
  105. username = "%s%s" % (name[:3], age)
  106. password = input("{?} Password: ")
  107.  
  108. password = base64.b64encode(hashlib.sha1(password.encode()).digest()).decode()
  109. # this bit first turns the password into a raw format (binary), then hashes the password into sha1 then base64 encoded it and decodes
  110. # it from binary (you could say this is unnecessary, but it provides added security)
  111. # it's a scalable system which allows for friendlier user input for subjects and questions by the config.ini file which allows you to
  112. # input more subjects and add varied difficulties for them and easily integrate them without much pain
  113.  
  114. # 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
  115.  
  116. # the registration/login system is pretty normal as the login system doesn't ever know the actual password for the user being logged
  117. # into, only the hash, so the program hashes the password given, then compares it to the hash that the database.dbs file has, if
  118. # they're #the same, then the user entered the correct password and if not, then they're different
  119.  
  120. # 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
  121.  
  122. # 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
  123. # fine, however this can be easily prevented by a trivial form of encryption
  124.  
  125.  
  126. if username not in args[1]:
  127. resp = poll_input("{*} Username not found in cached database, should I reload my database (y[es]/n[o])? ",
  128. ["yes", "y", "no", "n"],
  129. call_lower=True
  130. )
  131.  
  132. if 'y' in resp:
  133. with open(args[0]) as update:
  134. args[1] = json.loads(update.read())
  135.  
  136. if username not in (name[0] for name in args[1]):
  137. print("{*} Username not found in cached nor new database, please register.")
  138. return False
  139. print("{*} Found user in update database.")
  140.  
  141. elif 'n' in resp:
  142. return False
  143.  
  144. if base64.b64decode(args[1][username][3]) != base64.b64decode(password):
  145. print("{*} Incorrect password.")
  146. return False
  147.  
  148. return (args[1][username][0], args[1][username][1], args[1][username][2], args[1][username][3])
  149.  
  150.  
  151. config_file = "config.ini"
  152. dbs_file = "database.dbs"
  153. results_file = "results.file"
  154.  
  155. if len(sys.argv) >= 2:
  156. config_file = sys.argv[1]
  157.  
  158. if len(sys.argv) >= 3:
  159. dbs_file = sys.argv[2]
  160.  
  161. if len(sys.argv) == 4:
  162. results_file = sys.argv[3]
  163.  
  164. #
  165. ## Add your own subject into the config.ini
  166. ## with the following template
  167. ## "Subject Name": {
  168. ## "Hard": "path/to/file.csv",
  169. ## "Medium": "path/to/file.csv",
  170. ## "Easy": "path/to/file.csv",
  171. ## }
  172. #
  173. ## Assure that all of the contents are enclo
  174. ## sed inside {...} so that it can be read
  175. ## safely by the computer without causing
  176. ## errors
  177. #
  178.  
  179.  
  180. try:
  181. with open(config_file) as config:
  182. try:
  183. questions = json.loads(config.read())
  184. except json.decoder.JSONDecodeError:
  185. print("{*} Your '%s' file has syntax errors in it!" % config_file)
  186. sys.exit(1)
  187. except FileNotFoundError:
  188. print("{*} '%s' doesn't exist, please create it." % config_file)
  189. sys.exit(1)
  190.  
  191. current_user = []
  192.  
  193. users = {}
  194. fn_map = {
  195. "register": register,
  196. "login": login
  197. }
  198. grade_map = {
  199. 0: 'U',
  200. 10: 'F',
  201. 20: 'F',
  202. 30: 'F',
  203. 40: 'E',
  204. 50: 'D',
  205. 60: 'C',
  206. 70: 'B',
  207. 80: 'A',
  208. 90: 'A',
  209. 100: 'A*'
  210. }
  211.  
  212.  
  213. try:
  214. with open(dbs_file) as database:
  215. try:
  216. users = json.loads(database.read())
  217. except json.decoder.JSONDecodeError:
  218. print("{*} Your '%s' file has syntax errors in it!" % dbs_file)
  219. sys.exit(1)
  220. except FileNotFoundError:
  221. with open(dbs_file, 'w') as dbs:
  222. dbs.write("{}") # the natural json.loads(...) breaks without this
  223.  
  224. resp = poll_input("{?} Would you like to register or login? ", ["register", "login"], call_lower=True)
  225.  
  226. data = fn_map[resp](dbs_file, users)
  227.  
  228. if not data:
  229. print("{*} Failed to %s." % resp)
  230. sys.exit(1)
  231.  
  232. print("{*} Successfully registered/logged into %s." % data[0]) # note: registering an account automatically logs you in
  233. current_user.extend(data)
  234.  
  235. current_user.append({
  236. k: {
  237. v_k: [0, 0] for v_k in v
  238. } for k, v in questions.items()
  239. }
  240. )
  241. # the above code looks a bit ugly however it simply creates a 'topic: {for topic difficulty in topic: (0_0, 0_1)}'
  242. # where 0_0 is the current test-score and 0_1 is the total questions answered
  243.  
  244. if os.path.exists(results_file): # bad practice
  245. resp = poll_input("{!} There is an existing '%s' file, would you like to load it (y[es]/n[o])? " % (
  246. results_file
  247. ),
  248. ["yes", 'y', "no", 'n'],
  249. call_lower=True)
  250. if 'y' in resp:
  251. with open(results_file) as read_data:
  252. temp_data = json.loads(read_data.read())
  253.  
  254. if temp_data[:-1] != current_user[:-1]:
  255. print("{*} Loading '%s' file with non-matching data, exiting..." % results_file)
  256. sys.exit(1)
  257.  
  258. current_user = temp_data
  259.  
  260. chosen_subject = []
  261.  
  262. while 1:
  263. print("Here are your quiz scores: ")
  264. for topic, v in current_user[-1].items():
  265. for diff, scores in v.items():
  266. print("%s-%s:\n\t%s%% for %s out of %s questions, thus achieving a(n) %s grade." % (
  267. diff,
  268. topic,
  269. int(100/scores[1])*scores[0] if scores[1] else 0,
  270. scores[0],
  271. scores[1],
  272. grade_map[round_nearest_ten(int(100/scores[1])*scores[0] if scores[1] else 0)])
  273. )
  274. print()
  275.  
  276. for topic, v in itertools.cycle(questions.items()):
  277. if chosen_subject:
  278. break
  279. # it basically makes an infinite for loop which loops over questions.items()'s key and value pairs over and over
  280. # so a = itertools.cycle([1, 2])
  281. resp = poll_input("{?} Would you like to take %s (y[es]/n[o])? " % topic, ["yes", 'y', "no", 'n', "quit", 'q'], call_lower=True)
  282.  
  283. if 'n' in resp:
  284. continue
  285. elif 'q' in resp:
  286. print("{*} Saving and exiting...")
  287. save_file(current_user, results_file)
  288. sys.exit(0)
  289.  
  290. for diff, file_ in v.items():
  291. 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)
  292.  
  293. if 'n' in resp:
  294. continue
  295. elif 'q' in resp:
  296. print("{*} Saving and exiting...")
  297. save_file(current_user, results_file)
  298. sys.exit(0)
  299.  
  300. chosen_subject = [topic, diff]
  301. file_path = questions[topic][diff]
  302. break
  303.  
  304. with open(file_path) as quiz:
  305. quiz = quiz.readlines()
  306.  
  307. current_user[-1][chosen_subject[0]][chosen_subject[1]][1] += len(quiz)
  308.  
  309. for question in quiz:
  310. qst, *options, correct = question.strip().rsplit(',', 5)
  311.  
  312. resp = poll_input("Question: '%s'\n%s\nSelect by index: " % (qst, '\n'.join("%s) %s" % (i+1, v) for i, v in enumerate(options))),
  313. range(1, len(options)+1),
  314. map_fn=int,
  315. catch=(TypeError, ValueError))
  316.  
  317. if not resp:
  318. print("{*} Invalid index chosen!")
  319. continue
  320. # 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
  321. # between range(1, len(options)+1) (how many options there are for the question), ask them to reenter it until they enter a valid
  322. # choice
  323.  
  324. # map_fn=int, tells the function to make every input the user takes in an integer, whereas normally it's a string, as range
  325. # (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
  326. # list full of integers [1, 2, 3]
  327.  
  328. # 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
  329. # arguments, and then an implicit function will be called to handle the exception.
  330. # "Question: '%s'\n%s\nSelect by index: " % (qst, '\n'.join("%s) %s" % (i+1, v) for i, v in enumerate(options))
  331.  
  332. # this itself just goes over each question and its corresponding index and formats it like
  333. # Question: 'this is the question'
  334. # 1) choice 1
  335. # 2) choice 2
  336. # 3) choice 3
  337. # Select by index: <input-here>
  338. # and the user would enter a number between 1 to 3
  339. # if it's any lower or bigger, it'll keep asking until it gets the required input
  340. # enumerate(list) simply returns an (index, value)-ified list
  341. # e.g.
  342. # lst = ["this", "that", "there"]
  343. # lst = enumerate(lst)
  344. # print(lst)
  345. # >>> [(0, "this"), (1, "that"), (2, "there")]
  346. correct = int(correct)+1
  347.  
  348. if resp == correct:
  349. print("{!} You got the question correct!")
  350. current_user[-1][chosen_subject[0]][chosen_subject[1]][0] += 1
  351. time.sleep(0.5)
  352. else:
  353. print("{!} Your answer was incorrect.")
  354. time.sleep(0.5)
  355.  
  356. save_file(current_user, results_file)
  357.  
  358. chosen_subject = []
Add Comment
Please, Sign In to add comment