Yesterday, employers got their full dashboard. Today, for day 75, I built the candidate side, everything a developer needs to find a job on DevBoard. Browsing listings, searching and filtering by tech stack and location, viewing job details, and submitting an application with a cover letter. By the end of today, the two sides of the platform will be talking to each other in a complete loop.
The Job Listings Page
The first thing I built was the public job listings page: the homepage of DevBoard for anyone who isn't logged in yet. This is the most important page in the app. It needs to show enough information to be useful at a glance without being overwhelming.
Each listing card shows the job title, company name, location, job type, experience level, and the tech stack as individual tags. The tech stack display was satisfying to implement; the get_tech_stack_list() helper method I wrote on the model yesterday paid off here. Looping over the list in the template and rendering each skill as a small tag looks clean and is immediately scannable.
I made a deliberate decision to show the salary range only when both a minimum and a maximum are provided. Partial salary information, just a minimum with no maximum or vice versa, looks incomplete and unprofessional. If an employer didn't fill in both fields, the salary section doesn't render at all. Small decision, better experience.
Search and Filtering
This was the most technically interesting part of today. The search and filter system needed to handle multiple simultaneous filters: keyword search, location, job type, experience level, and combine them cleanly.
The approach I took was building the queryset progressively in the view. Start with all active jobs, then apply each filter only if the corresponding value was submitted. This means a user searching for "Django" in "Karachi" with "Full Time" selected gets all three filters applied at once, while a user who only types a keyword gets just the keyword filter. Each filter is optional, and they stack.
Keyword search uses Django's Q objects, something I hadn't used before today. Q objects let you combine filter conditions with OR logic, which is exactly what keyword search needs. A search for "Python" should match jobs where Python appears in the title, the description, or the tech stack, not just the title. Without Q objects, you'd need separate queries and manual deduplication. With them, it's one clean query.
The filter form stays filled in after submission, and the search inputs show whatever the user typed. This is handled by passing the current filter values back to the template in the context and binding them to the form fields. It sounds obvious, but easy to forget, and it makes the experience feel polished.
Job Detail Page
The job detail page is accessible to everyone, logged in or not. It shows the full job description, all the details, and the tech stack. At the bottom, there's an apply button.
For unauthenticated users, the apply button says "Login to Apply" and links to the login page. For logged-in employers, the button doesn't appear at all; employers don't apply to jobs. For candidates who have already applied, the button is replaced with an "Applied" badge showing their current application status. Only candidates who haven't applied yet see the actual apply button.
Getting these four states right in the template required careful use of {% if %} conditions and passing the right context from the view. The view checks whether the current user has an existing application for this job and passes that information to the template, so the template itself stays clean; it just checks a boolean flag rather than making its own database queries.
The Application Flow
Clicking apply takes the candidate to a simple application form, just a cover letter text area. The job details are shown alongside the form so candidates can reference them while writing.
The view has one important guard: if a candidate tries to apply to the same job twice by manipulating the URL, the unique_together constraint on the Application model catches it at the database level, and the view handles the integrity error gracefully with a message rather than a 500 error.
After submitting, the candidate is redirected back to the job detail page, where they now see the "Applied" badge with a "Pending" status. That immediate feedback matters; the candidate knows their application went through.
Candidate Dashboard
I added a simple candidate dashboard: a page showing all the jobs a candidate has applied to, with the current status of each application. Pending, reviewed, accepted, rejected, the status the employer sets on their side is immediately visible here.
This closed the loop between the two sides of the platform. An employer reviews an application and updates the status. The candidate refreshes their dashboard and sees the update. Two separate interfaces, one shared data layer.
A Problem I Hit
Midway through building the search feature, I ran into an issue with the queryset returning duplicate results. When a search term matched both the title and the tech stack of the same job, that job appeared twice in the results.
The fix was adding .distinct() to the queryset. Django's Q object queries with OR conditions can produce duplicates when they match multiple fields on the same object. .distinct() removes them. Simple fix, but it took me a few minutes to figure out why the same job was showing up twice in the results.
What DevBoard Can Do Now
At the end of day 75, DevBoard is a functioning two-sided platform:
Employers can:
- Register with a company profile
- Post, edit, and delete job listings
- View all applications per listing
- Update application status
Candidates can:
- Register with skills and experience
- Browse all active job listings
- Search and filter by keyword, location, job type, and experience level
- View full job details
- Apply with a cover letter
- Track all their applications and statuses
Tomorrow: the REST API with DRF and UI polish before deployment on day 77.
Thanks for reading. Feel free to share your thoughts!
Top comments (0)