├── index_card.gif ├── flashcard_data.pickle ├── README.md └── Flashcard.py /index_card.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwardle/Flashcard/HEAD/index_card.gif -------------------------------------------------------------------------------- /flashcard_data.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwardle/Flashcard/HEAD/flashcard_data.pickle -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flashcard 2 | A simple python 3.6.3 flashcard manager 3 | 4 | Author: Lawrence Wardle 5 | 6 | **Purpose:** 7 | 8 | This is a small first project for the purpose of: 9 | 1) Practicing relatively simple python code 10 | 2) Creating a useful educational tool 11 | 12 | The program will allow users to create, label, and save/load flashcards that persist between sessions. Individual cards will be automatically marked for review at specific times, described in more detail below. The cards will be displayed front side (question/prompt) first, then both sides, then manually marked correct/incorrect. The cards will be saved via the pickle module, and the interface will be designed with tKinter. 13 | 14 | The educational value of this simple program is in the way it manages the flashcards, based on the Leitner system. In the Leitner system of flashcards, cards are kept in a series of boxes. Each box carries a progressively increasing review delay, e.g. the first box is reviewed every day, the second box every other day, the third box once a week, etc. If a card is marked correct, it progresses to a box of less frequent review. A card marked incorrect will be sent back to the first box. Through this method, the user reviews everything regularly, but spends the vast majority of their time working on the items that actually need work. Also, there are thought to be benefits to reviewing material just before it is forgotten (as opposed to when the material is still remembered clearly, or after forgetting it). The incremental spacing of repetition may serve as another benefit of using this system of flashcard management. 15 | 16 | This program will be a small improvement over the Leitner system. The Leitner system reviews whole boxes at a time out of necessity; it is a system designed for physical flashcards. This program can iterate over flashcards individually, marking them for review based on the time they entered their current box. 17 | 18 | **Known Issues:** 19 | 20 | Trying to import a card from a .csv list will not work correctly if that card's response/prompt include a comma. 21 | 22 | **Additional features to put in later:** 23 | 24 | Ability to tag cards and ability to sort by tag 25 | 26 | Statistics about individual flashcards and overall performance 27 | 28 | Allow image/sound input for flashcards 29 | 30 | Allow for multi-sided flashcards 31 | 32 | ~~Allow for reversible flashcards~~ Accomplished! 33 | 34 | Nicer GUI and animated graphics 35 | 36 | Make into a mobile app 37 | -------------------------------------------------------------------------------- /Flashcard.py: -------------------------------------------------------------------------------- 1 | import pickle, datetime, random, os, csv 2 | from tkinter import filedialog 3 | from tkinter import * 4 | 5 | root = Tk() 6 | 7 | #TODO: define classes 8 | class Flash_Card(object): 9 | def __init__(self, side_1, side_2): 10 | self.side_1 = side_1 11 | self.side_2 = side_2 12 | self.location = 0 #refers to index location of current box within all_boxes 13 | self.review_flag = True #cards are created with the last review set as the date of creation, but the review flag on. 14 | self.last_review = datetime.datetime.now() 15 | 16 | class Box(object): 17 | def __init__(self, name, review_delay): 18 | self.name = name 19 | self.review_delay = datetime.timedelta(days=review_delay) 20 | 21 | def assignReviewDate(self): 22 | pass 23 | 24 | #TODO: save/load functions 25 | def saveFunction(): 26 | pickle_out = open(filedialog.asksaveasfilename(initialdir = "/",title = "Select file",filetypes = (("PICKLE files","*.pickle"),("all files","*.*"))), "wb") 27 | pickle.dump(all_cards, pickle_out) 28 | pickle_out.close() 29 | 30 | def loadPrompt(): 31 | load_window = Toplevel() 32 | warning = Label(load_window, text = "Are you sure you want to load?\nThis will discard the current data.") 33 | affirm_button = Button(load_window, text = "Yes", command = lambda: loadFunction(load_window)) 34 | cancel_button = Button(load_window, text = "No", command = lambda: load_window.destroy()) 35 | 36 | warning.grid(row = 0, column = 0, columnspan = 2, padx = 20, pady = 10) 37 | affirm_button.grid(row = 1, column = 0) 38 | cancel_button.grid(row = 1, column = 1) 39 | 40 | def loadFunction(load_window): 41 | global all_cards 42 | pickle_in = open(filedialog.askopenfilename(initialdir = "/",title = "Select file",filetypes = (("PICKLE files","*.pickle"),("all files","*.*"))), "rb") 43 | all_cards = pickle.load(pickle_in) 44 | generateStack() 45 | cards_left_count.set(len(study_stack)) 46 | cards_total_count.set(len(all_cards)) 47 | 48 | #TODO: button functions 49 | def markCorrect(): 50 | if len(study_stack) == 0: 51 | return 52 | study_stack[0].review_flag = False 53 | study_stack[0].location += 1 54 | card_back.config(text = "") 55 | study_stack[0].last_review = datetime.datetime.now() 56 | study_stack.remove(study_stack[0]) 57 | cards_left_count.set(len(study_stack)) 58 | if len(study_stack) == 0: 59 | card_back.config(text = "Don't forget to save!") 60 | card_front.config(text = "Congrats! You're done for today!") 61 | return 62 | new_prompt = study_stack[0].side_1 63 | card_front.config(text = new_prompt) 64 | 65 | def markIncorrect(): 66 | if len(study_stack) == 0: 67 | return 68 | study_stack[0].location = 0 69 | card_back.config(text = "") 70 | study_stack[0].last_review = datetime.datetime.now() 71 | study_stack.remove(study_stack[0]) 72 | cards_left_count.set(len(study_stack)) 73 | if len(study_stack) == 0: 74 | card_back.config(text = "Don't forget to save!") 75 | card_front.config(text = "Congrats! You're done for today!") 76 | return 77 | new_prompt = study_stack[0].side_1 78 | card_front.config(text = new_prompt) 79 | 80 | def showAnswer(): 81 | if len(study_stack) == 0: 82 | return 83 | new_text = study_stack[0].side_2 84 | card_back.config(text = new_text) 85 | 86 | def setReviewFlags(): 87 | for card in all_cards: 88 | review_delay = all_boxes[card.location].review_delay 89 | last_review = card.last_review 90 | if last_review + review_delay < datetime.datetime.now(): 91 | card.review_flag = True 92 | 93 | def deleteCard(window, card): 94 | all_cards.remove(card) 95 | study_stack.remove(card) 96 | cards_left_count.set(len(study_stack)) 97 | cards_total_count.set(len(all_cards)) 98 | window.destroy() 99 | card_back.config(text = "") 100 | if len(study_stack) == 0: 101 | card_front.config(text = "Congrats! You're done for today!") 102 | card_back.config(text = "Don't forget to save!") 103 | return 104 | else: 105 | new_prompt = study_stack[0].side_1 106 | card_front.config(text = new_prompt) 107 | return 108 | 109 | def generateStack(): 110 | global study_stack 111 | setReviewFlags() 112 | for card in all_cards: 113 | if card.review_flag == True: 114 | study_stack.append(card) 115 | random.shuffle(study_stack) 116 | if len(study_stack) == 0: 117 | card_front.config(text = "Congrats! You're done for today!") 118 | card_back.config(text = "Don't forget to save!") 119 | return 120 | else: 121 | new_prompt = study_stack[0].side_1 122 | card_front.config(text = new_prompt) 123 | 124 | def flipStack(): 125 | for card in all_cards: 126 | old_front = card.side_1 127 | old_back = card.side_2 128 | card.side_1 = old_back 129 | card.side_2 = old_front 130 | card_front.config(text = study_stack[0].side_1) 131 | 132 | def finalizeCard(card_prompt, card_response, creation_window): 133 | new_card = Flash_Card(card_prompt.get(), card_response.get()) 134 | all_cards.append(new_card) 135 | creation_window.destroy() 136 | cards_total_count.set(len(all_cards)) 137 | 138 | def creationPopup(): 139 | creation_window = Toplevel() 140 | side_one_prompt = Label(creation_window, text = "Prompt: ") 141 | side_two_prompt = Label(creation_window, text = "Response: ") 142 | card_prompt = StringVar() 143 | card_response = StringVar() 144 | side_one_entry = Entry(creation_window, textvariable = card_prompt) 145 | side_two_entry = Entry(creation_window, textvariable = card_response) 146 | finalize_button = Button(creation_window, text = "Finalize Card", command = lambda: finalizeCard(card_prompt, card_response, creation_window)) 147 | 148 | side_one_prompt.grid(row = 0, column = 0) 149 | side_two_prompt.grid(row = 1, column = 0) 150 | side_one_entry.grid(row = 0, column = 1, columnspan = 3) 151 | side_two_entry.grid(row = 1, column = 1, columnspan = 3) 152 | finalize_button.grid(row = 2, column = 0, columnspan = 2) 153 | 154 | def deletionPopup(): 155 | deletion_window = Toplevel() 156 | warning = Label(deletion_window, text = "Are you sure?") 157 | affirm_button = Button(deletion_window, text = "Yes", command = lambda: deleteCard(deletion_window, study_stack[0])) 158 | cancel_button = Button(deletion_window, text = "No", command = lambda: deletion_window.destroy()) 159 | 160 | warning.grid(row = 0, column = 0, columnspan = 2, padx = 20, pady = 10) 161 | affirm_button.grid(row = 1, column = 0) 162 | cancel_button.grid(row = 1, column = 1) 163 | 164 | def importCards(): 165 | incoming_cards_file = open(filedialog.askopenfilename(initialdir = "/",title = "Select file",filetypes = (("TEXT files","*.txt"),("all files","*.*")))) 166 | incoming_cards_reader = csv.reader(incoming_cards_file) 167 | incoming_card_data = [] 168 | for row in incoming_cards_reader: 169 | incoming_card_data.append(row) 170 | for card in incoming_card_data: 171 | all_cards.append(Flash_Card(card[0], card[1])) 172 | generateStack() 173 | cards_left_count.set(len(study_stack)) 174 | cards_total_count.set(len(all_cards)) 175 | 176 | def exportCards(): 177 | outputFile = open(filedialog.asksaveasfilename(initialdir = "/",title = "Select file",filetypes = (("TEXT files","*.txt"),("all files","*.*"))), 'w', newline='') 178 | card_data = [] 179 | for card in all_cards: 180 | card_data.append([card.side_1, card.side_2]) 181 | outputWriter = csv.writer(outputFile) 182 | for card in card_data: 183 | outputWriter.writerow(card) 184 | outputFile.close() 185 | 186 | #TODO: create variables 187 | 188 | all_boxes = [] 189 | 190 | for i in range(1, 7): 191 | all_boxes.append(Box(i, (2 ** (i-1)) )) #creates boxes to be reviewed every 1|2|4|8|16|32 days 192 | 193 | all_cards = [] 194 | study_stack = [] 195 | try: 196 | index_card_image = PhotoImage(file = "index_card.gif") 197 | except: 198 | image_failure_prompt = Toplevel() 199 | warning = Label(image_failure_prompt, text = "I couldn't find index_card.gif. Could you please find it for me?") 200 | warning.pack() 201 | index_card_image = PhotoImage(file = filedialog.askopenfilename(initialdir = "/",title = "Select file",filetypes = (("GIF files","*.gif"),("all files","*.*")))) 202 | image_failure_prompt.destroy() 203 | 204 | cards_left_count = IntVar() 205 | cards_left_count.set(len(study_stack)) 206 | cards_total_count = IntVar() 207 | cards_total_count.set(len(all_cards)) 208 | 209 | #TODO: create GUI elements 210 | card_front = Label(root, text = "", wraplength = 350, font = ("Arial", 24, "bold"), image = index_card_image, compound = CENTER) 211 | card_back = Label(root, text = "", wraplength = 350, font = ("Arial", 24, "bold"), image = index_card_image, compound = CENTER) 212 | mark_right = Button(root, text = "Mark Correct", command = markCorrect) 213 | mark_wrong = Button(root, text = "Mark Incorrect", command = markIncorrect) 214 | show_answer = Button(root, text = "Show Answer", command = showAnswer) 215 | flip_stack = Button(root, text = "Flip Stack", command = flipStack) 216 | create_new_card = Button(root, text = "Create New Card", command = creationPopup) 217 | cards_left = Label(root, text = "Number of cards left:") 218 | total_cards = Label(root, text = "Total number of cards:") 219 | cards_left_counter = Label(root, textvariable = cards_left_count) 220 | total_cards_counter = Label(root, textvariable = cards_total_count) 221 | delete_card = Button(root, text = "Delete This Card", command = deletionPopup) 222 | save_button = Button(root, text = "Save", command = saveFunction) 223 | load_button = Button(root, text = "Load", command = loadPrompt) 224 | import_cards = Button(root, text = "Import Cards From .TXT File", command = importCards) 225 | export_cards = Button(root, text = "Export Cards To .TXT File", command = exportCards) 226 | 227 | #TODO: arrange GUI elements 228 | card_front.grid(row = 0, column = 0, rowspan = 6) 229 | card_back.grid(row = 0, column = 1, rowspan = 6) 230 | delete_card.grid(row = 4, column = 3) 231 | create_new_card.grid(row = 4, column = 2) 232 | cards_left.grid(row = 2, column = 2) 233 | show_answer.grid(row = 0, column = 2) 234 | flip_stack.grid(row = 0, column = 3) 235 | cards_left_counter.grid(row = 2, column = 3) 236 | total_cards_counter.grid(row = 3, column = 3) 237 | mark_right.grid(row = 1, column = 2) 238 | mark_wrong.grid(row = 1, column = 3) 239 | total_cards.grid(row = 3, column = 2) 240 | save_button.grid(row = 5, column = 2) 241 | load_button.grid(row = 5, column = 3) 242 | export_cards.grid(row = 6, column = 2) 243 | import_cards.grid(row = 6, column = 3) 244 | 245 | root.mainloop() 246 | --------------------------------------------------------------------------------