Learn
08.5 | PROJECT: A DESKTOP STUDY PLANNER
01 | THE BRIEF: TURN INTENTIONS INTO TASKS
A learner needs to record a subject, a specific study task, a due date and planned minutes. They want to mark tasks complete, find open work and reopen a task if necessary. Those records must survive closing the window.
A separate focus timer helps with a short work session. Its countdown should not prevent editing the task list or moving the window. Planned minutes describe intention, not proof that the work actually happened.
Download the complete project below. It uses Tkinter, SQLite and other standard modules. No third-party package is required, but your Python installation must include Tk support and have a desktop display. Use Python 3.10 or later.
Download the desktop study planner02 | DEFINE THE ACCEPTANCE RULES
Subjects and task titles must be trimmed, non-blank text. The subject limit is forty characters and the task limit is eighty. Due dates must be real YYYY-MM-DD dates. Planned minutes must be a whole number from one to 240.
The task list shows all, open or completed records in due-date then ID order. Completion and reopening update the selected record. Deletion asks for confirmation because it permanently removes that record.
The focus timer accepts one to 120 whole minutes. Starting again replaces its deadline, cancelling stops it, and closing the application cancels scheduled refresh work. Saved tasks persist; the timer and current selection do not.
03 | SPLIT MODEL FROM INTERFACE
model.py defines StudyStore for validation and database operations, plus FocusTimer for elapsed-time rules. app.py creates the widgets and connects their callbacks to those services. test_model.py checks the core using temporary databases and a supplied clock.
A model owns meaningful data and rules. A view shows it, and callbacks coordinate user requests. This small separation is not a promise of one universal architecture; it makes this project understandable and testable.
Do not put input() inside a GUI callback to request missing data. Read the existing form controls and display feedback instead. The user should remain within the interface they opened.
| File | Responsibility |
|---|---|
| model.py | StudyStore, validation and FocusTimer |
| app.py | Tkinter widgets, selection, callbacks and rendering |
| test_model.py | 13 automated tests without opening a GUI |
| study.sqlite3 | Created beside app.py to retain tasks |
| README.txt | Setup, workflow, limitations and manual checks |
04 | RUN A SMALL END-TO-END TRIAL
Extract the download, select the intended interpreter and run the commands below in its folder. The model tests do not need a display, but app.py does.
Add “Computer Science”, “Trace a nested loop”, 2026-10-15 and 25 minutes. Select its row and click Complete selected. Change the filter to completed, then reopen it.
Close the application and run it again. The accepted task should still be present because a committed SQLite file stores it. This restart test is different from refreshing the browser illustration.
TERMINAL | PROJECT FOLDER
python -m tkinter python app.py python -m unittest discover -s . -v
05 | GIVE A ROW ITS REAL RECORD ID
A displayed row is not the database record’s identity. Sorting or filtering can change its position. The project inserts each Treeview item with iid=str(row["id"]), keeping a stable connection to the database key.
selected_id obtains the selected item identifier and converts it to an integer. With no selection it shows a helpful message and returns None. Completion callbacks stop there instead of accidentally changing some default record.
After a refresh, the code restores the previous selection only if that ID remains in the filtered list. Completing a task in the open filter makes it disappear from that view, which is expected rather than data loss.
PYTHON | EXCERPT FROM THE UI, NOT A STANDALONE FILE
selection = self.tree.selection()
if selection:
task_id = int(selection[0])
self.store.set_done(task_id, True)
self.refresh()06 | SAVE BEFORE CLEARING THE FORM
The add callback reads StringVar values, converts minutes to int and calls store.add. The store checks text, date and allowed minutes before inserting with bound parameters.
If validation or a SQLite operation fails, the callback reports “Not saved” and leaves the task field available for correction. Only after successful storage does it clear the task text and refresh the report.
refresh reads rows first, then replaces displayed items. A read error should not erase the last successful display and pretend the database is empty. The callback reports the failure instead.
07 | MAKE THE TIMER A SMALL INDEPENDENT MODEL
FocusTimer stores a deadline measured on its supplied clock. remaining calculates the duration still available and rounds upward to show a positive second until expiry. At the deadline it clears the timer.
The default clock is time.monotonic. In a test, a simple callable supplies a controlled number so sixty seconds can pass instantly without sleeping. The sample below belongs beside the downloaded model.py.
The GUI uses one recurring after callback to refresh the display about every 250 milliseconds. It does not start an additional callback chain each time Start is pressed.
from model import FocusTimer
now = [100.0]
timer = FocusTimer(clock=lambda: now[0])
timer.start(1)
print("Start:", timer.remaining())
now[0] = 130.5
print("Later:", timer.remaining())
now[0] = 160.0
print("Deadline:", timer.remaining())EXPECTED OUTPUT
Start: 60 Later: 30 Deadline: 0
08 | EXPLORE TASK STATE
This page-session model illustrates add, complete and reopen. It does not open SQLite and its records disappear on page reload. Use the downloaded desktop app for persistent records and the actual timer.
09 | TEST THE MODEL AND THE HUMAN WORKFLOW
The thirteen automated tests check persistence, SQL-looking input as data, invalid text and dates, minute limits, completion/reopening, deletion, missing IDs, fixed filters, ordering and clock-based countdown behaviour. Temporary databases protect real saved records.
Manual interface tests cover things the model alone cannot: Tab navigation, Enter submission, selecting the intended row, resizing, scrollbar operation and confirmation dialogs. Cancel deletion and check the row remains.
Start a one-minute timer, keep using the form and close while it is running. Reopen: task records remain, but the focus session starts fresh. The project makes that data-lifetime distinction visible.
10 | HAND OVER AND EXTEND DELIBERATELY
The database is study.sqlite3 beside app.py. Back it up with the application closed in this teaching setup. Keep it out of public Git history, together with environments and real private data.
This is a local single-user learning tool without accounts, encrypted storage or automatic schema migrations. If you add categories or priorities, plan how existing rows will move to the changed schema. CREATE TABLE IF NOT EXISTS will not update old columns.
Useful extensions include editing a selected task, subject filters, CSV export and keyboard shortcuts. Write the expected behaviour and rejection cases first, then implement one feature and rerun existing tests.
OFFICIAL REFERENCE
Use the worked examples first, then consult the official documentation to check widget, input or drawing options. Match it to the version selected in your project environment.
PUT YOUR SKILLS TO WORK
Use the Section 8 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 8 challenges Python course navigation