DEV Community

Cover image for Day 73 of 100 Days Of Code — Building DevBoard: Project Setup and Authentication
M Saad Ahmad
M Saad Ahmad

Posted on

Day 73 of 100 Days Of Code — Building DevBoard: Project Setup and Authentication

Django learning is done. Today I started building a real, portfolio-worthy project called DevBoard, a developer job board where companies post jobs and developers find them. Day 73 was about laying the foundation right: project setup, a custom user model with two distinct roles, and a complete authentication system. Everything that comes after depends on getting this right first.


What is DevBoard?

DevBoard is a job board platform built specifically for developers. Two types of users:

  • Employers — companies that post job listings and review applications
  • Candidates — developers who browse listings, search by stack and location, and apply

By the end of day 77, DevBoard will be a fully deployed, working web application with both an HTML interface and a REST API.


Project Setup

python -m venv env
source env/bin/activate
pip install django djangorestframework python-decouple dj-database-url Pillow
django-admin startproject devboard
cd devboard
python manage.py startapp accounts
python manage.py startapp jobs
Enter fullscreen mode Exit fullscreen mode

Two apps:

  • accounts — handles everything user-related: registration, login, profiles, roles
  • jobs — handles job listings, applications, and search

Registered both in settings.py:

INSTALLED_APPS = [
    ...
    'rest_framework',
    'rest_framework.authtoken',
    'accounts',
    'jobs',
]
Enter fullscreen mode Exit fullscreen mode

Environment setup right from day one:

# .env
SECRET_KEY=your-secret-key-here
DEBUG=True
DATABASE_URL=sqlite:///db.sqlite3
ALLOWED_HOSTS=localhost,127.0.0.1
Enter fullscreen mode Exit fullscreen mode
# settings.py
from decouple import config, Csv
import dj_database_url

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', cast=bool, default=False)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
DATABASES = {'default': dj_database_url.config(default=config('DATABASE_URL'))}
Enter fullscreen mode Exit fullscreen mode

Custom User Model

This is the most important decision in any Django project, and it must be made before the first migration. Django's built-in User model works fine for basic projects, but once you need custom fields like a role, you need a custom user model.

If you start with Django's built-in User and try to swap it out later, it breaks migrations. Always set up a custom user model before running any migrations.

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    EMPLOYER = 'employer'
    CANDIDATE = 'candidate'

    ROLE_CHOICES = [
        (EMPLOYER, 'Employer'),
        (CANDIDATE, 'Candidate'),
    ]

    role = models.CharField(max_length=20, choices=ROLE_CHOICES)
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
    location = models.CharField(max_length=100, blank=True)

    def is_employer(self):
        return self.role == self.EMPLOYER

    def is_candidate(self):
        return self.role == self.CANDIDATE

    def __str__(self):
        return f"{self.username} ({self.role})"
Enter fullscreen mode Exit fullscreen mode

AbstractUser gives us everything Django's built-in user has: username, password, email, is_staff, is_active, plus the custom fields on top.

Tell Django to use this model instead of the default:

# settings.py
AUTH_USER_MODEL = 'accounts.User'
Enter fullscreen mode Exit fullscreen mode

Now run the first migration:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Employer Profile

Employers need a company profile name, website, logo, description. This lives in a separate model linked to the User:

class EmployerProfile(models.Model):
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name='employer_profile'
    )
    company_name = models.CharField(max_length=200)
    company_website = models.URLField(blank=True)
    company_logo = models.ImageField(upload_to='logos/', blank=True, null=True)
    company_description = models.TextField(blank=True)
    founded_year = models.IntegerField(blank=True, null=True)

    def __str__(self):
        return self.company_name
Enter fullscreen mode Exit fullscreen mode

OneToOneField means one user has exactly one employer profile. related_name='employer_profile' lets you access it as user.employer_profile from anywhere.


Candidate Profile

Candidates have a different profile, tech stack, experience, resume:

class CandidateProfile(models.Model):
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name='candidate_profile'
    )
    skills = models.TextField(help_text="Comma separated skills")
    experience_years = models.IntegerField(default=0)
    resume = models.FileField(upload_to='resumes/', blank=True, null=True)
    portfolio_url = models.URLField(blank=True)
    github_url = models.URLField(blank=True)
    linkedin_url = models.URLField(blank=True)

    def get_skills_list(self):
        return [skill.strip() for skill in self.skills.split(',')]

    def __str__(self):
        return f"{self.user.username}'s profile"
Enter fullscreen mode Exit fullscreen mode

get_skills_list() is a convenience method that splits a comma-separated list of skills into a Python list, useful for displaying tags in templates.


Auto-Creating Profiles with Signals

When a user registers, the corresponding profile should be created automatically based on their role:

# accounts/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User, EmployerProfile, CandidateProfile


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        if instance.role == User.EMPLOYER:
            EmployerProfile.objects.create(user=instance)
        elif instance.role == User.CANDIDATE:
            CandidateProfile.objects.create(user=instance)
Enter fullscreen mode Exit fullscreen mode
# accounts/apps.py
from django.apps import AppConfig


class AccountsConfig(AppConfig):
    name = 'accounts'

    def ready(self):
        import accounts.signals
Enter fullscreen mode Exit fullscreen mode

Registration Forms

Two separate registration forms, one for each role:

# accounts/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import User


class EmployerRegistrationForm(UserCreationForm):
    company_name = forms.CharField(max_length=200)
    company_website = forms.URLField(required=False)

    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']

    def save(self, commit=True):
        user = super().save(commit=False)
        user.role = User.EMPLOYER
        if commit:
            user.save()
            user.employer_profile.company_name = self.cleaned_data['company_name']
            user.employer_profile.company_website = self.cleaned_data.get('company_website', '')
            user.employer_profile.save()
        return user


class CandidateRegistrationForm(UserCreationForm):
    skills = forms.CharField(
        help_text="Enter your skills separated by commas",
        widget=forms.TextInput(attrs={'placeholder': 'Python, Django, React'})
    )
    experience_years = forms.IntegerField(min_value=0, initial=0)

    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']

    def save(self, commit=True):
        user = super().save(commit=False)
        user.role = User.CANDIDATE
        if commit:
            user.save()
            user.candidate_profile.skills = self.cleaned_data['skills']
            user.candidate_profile.experience_years = self.cleaned_data['experience_years']
            user.candidate_profile.save()
        return user
Enter fullscreen mode Exit fullscreen mode

Both forms set role automatically on save; the user never selects it manually. Role is determined by which registration page they use.


Registration Views

# accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from .forms import EmployerRegistrationForm, CandidateRegistrationForm


def register_employer(request):
    if request.method == 'POST':
        form = EmployerRegistrationForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('employer_dashboard')
    else:
        form = EmployerRegistrationForm()
    return render(request, 'accounts/register_employer.html', {'form': form})


def register_candidate(request):
    if request.method == 'POST':
        form = CandidateRegistrationForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('job_list')
    else:
        form = CandidateRegistrationForm()
    return render(request, 'accounts/register_candidate.html', {'form': form})
Enter fullscreen mode Exit fullscreen mode

After registration, employers go to their dashboard; candidates go to the job listings. The redirect is role-aware.


Role-Based Access Decorators

Instead of checking request.user.role in every view manually, custom decorators keep things clean:

# accounts/decorators.py
from django.shortcuts import redirect
from functools import wraps


def employer_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect('login')
        if not request.user.is_employer():
            return redirect('job_list')
        return view_func(request, *args, **kwargs)
    return wrapper


def candidate_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect('login')
        if not request.user.is_candidate():
            return redirect('job_list')
        return view_func(request, *args, **kwargs)
    return wrapper
Enter fullscreen mode Exit fullscreen mode

Usage:

@employer_required
def post_job(request):
    ...

@candidate_required
def apply_for_job(request, pk):
    ...
Enter fullscreen mode Exit fullscreen mode

URL Configuration

# accounts/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    path('register/employer/', views.register_employer, name='register_employer'),
    path('register/candidate/', views.register_candidate, name='register_candidate'),
    path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]
Enter fullscreen mode Exit fullscreen mode
# devboard/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('accounts.urls')),
    path('', include('jobs.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Enter fullscreen mode Exit fullscreen mode

Admin Setup

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User, EmployerProfile, CandidateProfile


@admin.register(User)
class CustomUserAdmin(UserAdmin):
    list_display = ['username', 'email', 'role', 'is_active']
    list_filter = ['role', 'is_active']
    fieldsets = UserAdmin.fieldsets + (
        ('Role & Profile', {'fields': ('role', 'bio', 'avatar', 'location')}),
    )


admin.site.register(EmployerProfile)
admin.site.register(CandidateProfile)
Enter fullscreen mode Exit fullscreen mode

Where Things Stand After Day 73

The foundation is solid:

  • Custom user model with employer and candidate roles
  • Automatic profile creation on registration via signals
  • Separate registration flows for each role
  • Role-based decorators for protecting views
  • Environment variables configured from day one
  • Admin panel set up to manage users

Tomorrow: the employer side: posting jobs, managing listings, and viewing applications.

Top comments (0)