Skip to content

CHKSmith25/job-application-tracker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Job Application Tracker

Description

In One Sentence

A Flask web application that stores, displays, updates and dynamically sorts job application data to help users organise and prioritise their job search process.

Who is it for? Why would someone use it?

People that may use this are young adults / students who are often faced with a long list of job applications to get their first job. Even with Excel or Google Sheets, it's hard to keep track of important applications with varying urgency. This app helps solve the problem of organising, prioritising and keeping track of long lists of job applications.

Guiding Principle

At every step, ask: How is this better than using a spreadsheet? If a feature doesn’t improve clarity, speed, or insight, then reconsider its value.

This helped nurture my thoughts and encourage simplicity. Ultimately, I would just use a spreadsheet if the tracker wasn't better, and so asking this question for each feature helped shape the project direction.

What inspired the idea?

I'm about to approach applying to my first career-based role and will benefit from using something more supportive than Google Sheets to keep track of applications. This project felt like a perfect combination of building my portfolio while making something that I want to actually use: an ideal motivator.

Summary

User has the ability to register an account, login, log off and use the dashboard, once logged in. The dashboard includes a display table of all the uploaded applications and several filter options to narrow down the display. There is also a feature to add new applications, which is included in the dashboard as a modal. Additionally, each application (row of the table) has an edit button linking the user to a separate page where they can update information about each application.

Each application has its own dynamically calculated priority score and the entire display table is ordered in descending order based on these scores. The score is determined by an exponential decay equation (more detail below) that combines two main variables, namely: t = time until deadline and r = rating of application (out of ten).

This goes beyond a basic CRUD app with its dynamic prioritisation and decay logic, user authentication and filtering using query parameters.

Key Features

Database

Schema

Users table:

ID (PK int) | Email (str) | Password (str) | Created (datetime)

  • Updated / accessed with user authentication. All not nullable.

Job application table:

ID (PK int) | User ID (FK int) | Company (str) | Role Title (str) | Location (str) | Salary (int) | Company Website (str) | Application Link (str) | Status (str) | Date Applied (date) | Deadline (date) | Rating/10 (int) | Workplace (str) | Archived (bool) | Created (datetime) | Updated (datetime)

  • Constraints used for extra security. Status must be one of a selection of values, from 'Prospecting' to 'Interview' to 'Ghosted'. Rating must be between 0 and 10 also.

  • Company, role title, location, status & rating are not nullable (mandatory) and the rest are optional.

    • created_at, updated_at and archived are all not nullable as well, however they're automatically stored / don't require user interaction

User Authentication

Register/Login/Logout

  • Register route gets email, password and password confirmation from register.html, using request.form.get(). Checks made for empty and invalid inputs before entered into the database using SQLAlchemy's db.session.add().

  • Login gets email and password from login.html. Checks for validity, empty inputs and matches in database before storing the ID in the session using flask-session.

  • Logout clears session with session.clear().

Password Hashing, Email Validation & Login Required Decorator

  • Used werkzeug.security library for password hashing before storing in database. Used check_password_hash to check for correct password in login route. Password is case sensitive.

  • Used email_validator library to validate email inputs and return an error if not valid. Utilised try and except block for this security. Email made case insensitive with .lower().

  • Took strong inspiration from Flask Pallet projects to create a login_required decorator. This wraps around any Flask route to require the session id matches user_id. This allowed me to restrict access to pages, unless the user is logged into the session.

Job Application Display

HTML Table

  • Job application data queried in dashboard route GET request, using SQLAlchemy's method-chaining syntax, filtering for the session's user_id. This data is displayed in index.html template using Jinja loop and dot notation to access fields.

  • Jinja if statements used in HTML table to check for non-null values, then specific formatting applied. Salary converts from int (salary type in database) to standard currency display using "{:,.0f}".format() in the Jinja expression, while the company and application website links are put into tags. Filter operator '|' used for case where there are no applications to display: message displayed instead.

Filtering

  • Multiple filters for the display table available: by status; min/max rating values; and/or keyword search for company, role_title or location. Filtering requests done through dashboard route's GET method.

  • Used request.args.getlist for status filter, to allow for multiple status selections in one filter. Conditions relating to each filter are added to the list using the .append() method. Keyword search filters are added to the list using SQLAlchemy or_() function and .ilike(f"%{q}%") to search across multiple fields.

  • conditions list added to query filter using the unpacking operator: "*conditions".

Automatic Prioritisation

Compute Priority Function

  • Inside helpers.py the compute_priority function computes a unique priority score for any application at the current time. It takes two arguments, namely the single application SQLAlchemy object and the current date. Priority score is determined by two main parameters: urgency (time until deadline) and user rating (out of ten).

  • The time until deadline (days) is calculated by the difference between deadline date and current date.

The Equation

k = k0 * math.exp(-beta * (rating - r0))

p = round(100 * math.exp(-k * t), 1)

Where k0 = ln(2)/6, r0 = 5.5 & beta = 0.2

  • The design echoes ideas from scheduling theory. In particular, the Apparent Tardiness Cost (ATC) rule, Vepsäläinen & Morton (1987) uses an exponential decay to combine urgency and importance.

  • The idea was that urgency should dominate the prioritisation and should always be 100 at the deadline date, regardless of rating. This meant that the rating should influence the decay steepness rather than shifting the curve, hence including it inside of k.

priority score plots for different rating scores

Understanding k

  • Higher ratings result in smaller k, which causes a slower decay for p.

    • Highly rated applications still have high scores even when they're due in a lot of time.
  • Lower ratings result in higher k, which produces a faster decay

    • Despite this, because of the fine tuning of constants, application urgency dominates close to the deadline. This means that the effect is only more prominent with larger times to deadlines.

k vs rating

Dynamically Calculating Priority Score

  • Before loading the display table in index.html, the priority scores for each application are computed, in the dashboard route, and added to the application SQLAlchemy object. This avoids the need to save the scores and update them in the database itself, which is ideal as the priority scores will change as time progresses.

  • AI was used here to help adjust the parameters (such as h and β), visualise how changes affected the curves, explore alternative formulations (e.g. time shifts, multipliers, logistic curves), formalise the chosen exponential decay equation, and generate tables and charts to illustrate behaviour.

Add New Job Application

Frontend JavaScript and HTML

  • Clickable button ("open-add-modal") at top of index.html page to toggle the modal which contains the input form for the new job application.

  • Job Application form with multiple input fields, corresponding to the job_application table fields (see database schema above), wrapped in a division tag with a hidden class that's added/removed by the JavaScript. Required attribute is used on the data fields that are not nullable in the database. Modal contains close button and a submit button also.

  • A whole range of Tailwind CSS attributes are used in index.html and are detailed in a later section of this file.

  • Scalable vector graphics used for close button, mainly derived from Flowbite source (see sources section).

  • JavaScript at the bottom of the index.html file, that uses document.getElementById() alongside .addEventListener() to create dynamic responses to the add application and close buttons. These remove or add the hidden class attribute from the division tag that wraps around the modal.

App.py Backend

  • Dashboard POST request uses request.form.get to retrieve all form input data. Data is cleaned: all non-nullable data is checked for missing inputs, strings converted to integers or datetimes where necessary.

  • db.session.commit() contained within try block to catch errors.

  • db.session.rollback() to clear failed data in case of error.

Edit and Delete

Edit Button

  • Edit button added to each row of index.html as an tag. href for each edit button contains the unique app id for the row.

  • When clicked, it navigates user to the /update/id route, which renders the update form.

Update Form

  • Very similar form to the add application form, however current data values are displayed in the input boxes, and can be edited immediately.

  • If database has null values (can be the case for optional fields), the input displays an empty string.

    • Used " or ' ' " in the value assignment in update.html. This, combined with adding " or None " to the application.{field} assignment in app.py, prevented empty strings or "None" as a string being saved to the database. Rather, a raw None value is saved.
  • Edit and Delete button in the update.html form, both named "action" with either "update" or "delete" as their value. This gave the ability to separate them with if statements in the update route in app.py, to perform different actions within the same POST method request.

  • db.session.commit() used to commit the new field values to the database, if edit button selected.

  • db.session.delete() used to remove the application row from the database, if delete button selected.

Error Handling

Apology function

Very simple apology function, in helpers.py, used to print out error messages on a separate page (apology.html) in the case of something not working. This proved very useful for debugging and printing specific error messages when needed.

Tailwind CSS Attributes

Class Attributes

  • Large range of attributes used for formatting:
    • px, py, etc. | spacing
    • w-, h-, max-w- | container sizes
    • gap, grid-cols, etc. | responsive grid layout
    • flex, flex-wrap, etc. | form
    • justify-center, items-center | page alignment
    • bg-sky-800, hover:, etc. | colours

Other Attributes

  • data-modal-toggle | interaction for modal JavaScript
  • hidden | visibility interaction
  • overflow-x-auto | x scroll on table for small display window
  • tabindex | Modal tab interactivity

File Overview

  • app.py --> database initiation/setup, Flask routes, database queries. This is where I learned to use SQLAlchemy, building from my understanding of SQLite3. Learned more about decorators, GET and POST requests, SQLAlchemy syntax and database security measures such as check constraints.

  • helpers.py --> Contains functions to assist app.py: apology() for error handling; compute_priority() to get priority scores for each application; login_required() function with wrap decorator to restrict access.

  • requirements.txt --> Python dependencies list

  • Templates:

    • layout.html --> Base template with navbar and flash messages
    • index.html --> Dashboard page with display table, filtering, and add new application modal. Edit buttons linking to update page. Tailwind CSS attributes, used for style and HTML side security, added to form inputs (min, max, required fields). Flask route converts all relevant data from strings before database entry.
    • login.html --> Login page with simple login form (email and password). Additional security implemented in Flask route method=post.
    • register.html --> Registration page with simple form (email, password and confirm password). Route checks and cleans inputs for security.
    • update.html --> Edit application page. Similar format to add new application modal (in index.html), however with current values displayed, making edits clearer. Same HTML side security, as well as data handling in Flask route to protect database.
    • apology.html --> Error page with simple message.
  • docs: priority equation visualisation

    • k_vs_rating.png
    • priority_equation_curves.png
    • AI-written progress documents
  • Database tables created automatically using SQLAlchemy's db.create_all().

Design Choices

  • Flask: familiar and contains all the functionality for the scope of this project.
  • SQLAlchemy ORM: New learning with neat / simple syntax and readability, provides clean database operations and automatic table creation.
  • Tailwind CSS: New framework and slick appearance, from online examples (Flowbite for modal also new learning).
  • Server-side Flask-sessions: familiar, more secure than browser side, less size limitations, easier to debug/control/edit.
  • Modal form using JavaScript & HTML: Keeps index.html neater, so that focus can remain on display table but it's also quick and easy to add data too.
  • Edit form on separate page: Decided to create a new update route / page to handle edits. Considered keeping it all on the index.html page, as the modal form code could be repurposed for the edit form, however that would've involved 3 different submit buttons, hence several if statements in the dashboard route post request. Separating the update page kept the app.py routes neater, despite re-using much of the code for the add application form.

New Learnings (Organised by Key Features)

This section highlights my growth in understanding throughout the project and outlines the new concepts, technologies and skills I developed. It shows what I implemented and any broader learnings too.

User Authentication

Server Side Flask Sessions & Security

  • Learned about server-side sessions with Flask-session. Sessions remember user across HTML requests and server-side only stores the session ID in cookies. User ID is stored server-side which is more secure than browser side. SESSION_PERMANENT = False, sets the session as temporary so that it lasts as long as the browser is open. SESSION_TYPE = filesystem stores the session data in local directory.

  • Also discovered app.secret_key and how it cryptographically signs session data, so that when a cookie is sent to the browser, it has a signature based off the key that remains persistent through the session. If it gets tampered with and a different signature is sent back from the browser, the app rejects it. os.random(24) creates a random string of bytes each time the app is loaded (thus signs the user out each time app is opened).

Password Security

  • New functions from Werkzeug security library to check passwords during login and hash passwords during user registration.

Decorators

  • Custom @login_required decorator using functools.wraps around route functions to enforce session-based logins.
  • Decorators wrap around existing functions, adding additional behaviour without changing any of the code. They add logic before and/or after.

Email Verification

  • Learned how to use validate_email and EmailNotValidError to check emails inside a try and except block.

Job Entry Management (CRUD)

Flask Form Handling

  • Learned the use of request.form.get() for private HTML POST requests. This method returns None if the value is empty, which makes it easy to check for empty / invalid values and handle errors.

Redirect vs Render Template

  • post/redirect/get pattern:
    • render_template shows page immediately and keeps form submission data. For example, if a form submission has an error, render_template would reload the page whilst keeping the post request context.
    • Alternatively, redirect sends the user to the get request for a new page, helping avoid re-submission of data. This helped me understand needing to use redirect after form submission (post request).

Filtering & Search on the Dashboard

  • Learned how to build filters that can be applied together with SQLAlchemy, by using a conditions list. This meant I could keep the query flexible and only add filters if they were selected, rather than building lots of nested if statements.
  • Understood how request.args.getlist() requests multiple values from one filter input in index.html to be captured, making the multi-select checkbox work properly.
  • Learned about the .ilike() function and the or_() operator for keyword searches across multiple fields. This broadened my understanding of how SQLAlchemy can mimic SQL conditions in Python.
  • Realised that filtering in a GET request is different from POST form handling, because GET queries send data to the URL string (ideal for filter - reusable), while POST is private and temporary in the request body.

Converting Values

  • Learned that all .get() method calls on the form or args object from HTML forms are strings. These need to be converted before saved in the database.
    • For dates, .strip() method can be used to avoid empty string values. datetime.strptime().date() can then convert string to Python date object, which then is database ready.
    • Salary needs to be converted from str to int but also adding a specific required range can catch out invalid input errors (where an extra 0 is added by accident).

Priority Score Model

  • Developed understanding of exponential decay models and how they can combine two parameters — urgency (time until deadline) and importance (rating).
  • Learned how to calculate differences between dates in Python to get the number of days left until a deadline.
  • Gained insight into how tuning parameters (such as h, β, and r₀) changes the steepness of the decay, and how urgency should dominate close to the deadline.
  • Experimented with different curve shapes and learned why exponential was a better fit than linear or logistic for this application.

UI Foundation (Tailwind CSS + Modal UX)

  • Learned to use Tailwind CSS utility classes to style forms and layouts within HTML tags, without writing additional CSS file.
  • Discovered the difference between relative and absolute positioning when making the modal work. I wanted the modal to appear in the centre and cover the page content underneath, with a shaded background too.
  • Understood when a modal makes sense (quick add form) versus when a dedicated page is clearer (editing existing data).

Database Design

  • Learned that SQLAlchemy automatically turns my Python classes into database tables. Also found out how things like nullable, unique, and check constraints help add security. Combination of security on app.py and HTML pages ensures a clean database.
  • Understood the value of defaults (like archived = False) and automatic timestamps for created/updated fields, to keep track of each application. These weren't directly utilised, however would be useful in more developed versions of the application.
  • Learned about indexing possibilities and how queries might be optimised later by indexing on combinations like (user_id, deadline).

Agile Planning & Sprints

My plan from the start was to use the agile methodology, as an example to showcase to prospective employers

  • At the beginning, I had a broad vision for the job tracker, with features such as: analytics, todos, CV tailoring, and integrations. This was more of a brainstorm than a practical plan, and I found it difficult to gauge how long things would take, as I didn't know how to make them.
  • I realised I was trying to do too much at once, so I focused on getting something basic working first. I learned that it's better to build small pieces and test them, rather than trying to build everything at once.
  • My sprints became structured around individual pages and features: first register, then login and logout, then dashboard, then update/edit. Each of these formed its own milestone.
  • Early sketches helped me keep sight of the main flow of the app. Referring to my guiding principle and thinking about what the user needed at each step, I avoided spending time on features that weren’t essential.
  • I learned the importance of iteration: building something basic, testing it, and then improving or expanding in the next sprint. This prevented me from getting stuck on perfectionism too early.
  • The outcome of this Agile approach was a clear, usable app that works end-to-end, with a backlog of improvements for the future.

Challenges & Debugging

  • Filtering logic in dashboard route

    • Problem: Had to accommodate any combination of filters to apply to the single database query, which would display the applications for the user. This left me stuck with multiple nested if statements and got messy.
    • Debug: Separate conditions for each filter request, added to a conditions list.
    • Learned: *conditions syntax (unpacking operator) to put list straight into the query. If no value for request.form.get, then it wouldn't be appended to the list.
  • Edit form inputting 'None' strings into database

    • Problem: Input tags included values that corresponded to application data. Some fields that were optional were stored as raw null values in the database, but then were getting updated to 'None' strings.
    • Debug: used the " or ' ' " notation in the value="..." of the input tags + the " or None " syntax in Flask route, for some of the optional entries, to convert empty string to raw None values in database.
    • Learned: Need to manage null values in the database carefully, in both ways. When updating using request.form.get(), the value will always be str type until converted.
  • Form appearance

    • Problem: input fields and buttons appearing immediately one after the other in the form.
    • Debug: As was the approach with all UX re-formatting: make changes to Tailwind attributes, use flask run, check visually, then adjust. Repeated this process multiple times. Used gap and grid-cols attributes for this specific case, to organise form inputs neatly.
    • Learned: Certain attributes are needed to go in certain divisions, else they won't work. For example, cell spacing and text class attributes are needed in each tag in an HTML table - applying attributes to them all through a
      tag that wraps around them doesn't work.
  • Type Error: 'User' object not subscriptable

    • Problem: registration would not work and would return this type error, as I was trying to access the SQLAlchemy object like it was a list (e.g. users[0].password_hash).
    • Debug: misunderstood what is returned by filter_by() and .first() - it is a particular SQLAlchemy object, rather than a Python list. Removing the indexing "[0]", fixed the issue.
    • Learned: SQLAlchemy uses dot notation to access fields of the database query object.
  • Status filter issue

    • Problem: only one status was being filtered for, despite selecting and submitting several in the index.html filter form.
    • Debug: deduced that the issue must be in how the form input was being received in app.py. The problem lay with the use of request.args.get(), which only retrieves a single value. Replacing get() with getlist() obtained a list of values from the multi-select status filter instead.
    • Learned: developed a greater understanding of how the browser and app communicate. The request object acts as the gate for the transfer of data from the app frontend to the backend. Also gained clarity on how GET parameters vary for multi-select inputs.
  • Priority score not updating

    • Problem: priority score was calculated and stored in the database originally, however it was unclear how it could be updated upon each re-loading of the page - the score became stale over time.
    • Debug: Used the compute_priority() function in helpers.py to dynamically add the priority score to the SQLAlchemy object using dot notation: application.priority_score = compute_priority(...). This done in dashboard route, without need for a priority field in the database.
    • Learned: Learned that you can attach custom fields to SQLAlchemy objects without the need to have them in the database, thus saving the data temporarily in memory and avoiding the need to interact with the database.

Areas for Improvement

  • Archive toggle: Add a simple toggle to each row. If archived, then the application is dead and automatically goes to the bottom of the table display
  • To-do lists: Additional table and linked to within the dashboard table display
  • Inline editing / updating: Using JavaScript to create cell-like inline editing (mimic Google Sheets)
  • AI API: To tailor CV and Cover Letter to Jobs
  • Analytics dashboard: Graphs to analyse progress, show pie charts for different fields, etc.
  • Calendar integration: Integrate deadlines into Google Calendar
  • Export database as CSV or PDF

AI Use & Sources

About

A Flask web application that stores, displays, updates and dynamically sorts job application data to help users organise and prioritise their job search process.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors