diff --git a/apps/jobs/admin.py b/apps/jobs/admin.py index 8c38f3f..25c25c5 100644 --- a/apps/jobs/admin.py +++ b/apps/jobs/admin.py @@ -1,3 +1,117 @@ from django.contrib import admin +from jobs.models import Company, JobPosting, JobApplication -# Register your models here. + +class JobApplicationInline(admin.TabularInline): + model = JobApplication + extra = 0 + readonly_fields = ("applicant", "status", "applied_at", "last_updated") + can_delete = False + + +@admin.register(Company) +class CompanyAdmin(admin.ModelAdmin): + list_display = ("name", "industry", "size", "location", "created_at") + search_fields = ("name", "industry", "location") + list_filter = ("industry", "size", "location") + readonly_fields = ("created_at",) + + +@admin.register(JobPosting) +class JobPostingAdmin(admin.ModelAdmin): + list_display = ( + "title", + "company", + "posted_by", + "job_type", + "experience_level", + "location", + "is_remote", + "status", + "featured", + "view_count", + "application_deadline", + "created_at", + ) + list_filter = ( + "job_type", + "experience_level", + "status", + "featured", + "is_remote", + "created_at", + "application_deadline", + "expires_at", + ) + search_fields = ( + "title", + "company__name", + "location", + "skills_required", + "posted_by__username", + ) + date_hierarchy = "created_at" + autocomplete_fields = ("company", "posted_by") + inlines = [JobApplicationInline] + readonly_fields = ("view_count", "created_at", "updated_at") + + fieldsets = ( + ( + "Basic Info", + { + "fields": ( + "title", + "company", + "posted_by", + "description", + "requirements", + "benefits", + "skills_required", + ) + }, + ), + ( + "Job Details", + { + "fields": ( + "job_type", + "experience_level", + "location", + "is_remote", + "salary_min", + "salary_max", + "salary_currency", + ) + }, + ), + ( + "Status & Metadata", + { + "fields": ( + "status", + "featured", + "view_count", + "application_deadline", + "expires_at", + "external_url", + "created_at", + "updated_at", + ) + }, + ), + ) + + +@admin.register(JobApplication) +class JobApplicationAdmin(admin.ModelAdmin): + list_display = ( + "job", + "applicant", + "status", + "applied_at", + "last_updated", + ) + list_filter = ("status", "applied_at", "last_updated") + search_fields = ("job__title", "applicant__username") + autocomplete_fields = ("job", "applicant") + readonly_fields = ("applied_at", "last_updated") diff --git a/apps/jobs/models.py b/apps/jobs/models.py index 71a8362..27ba6a3 100644 --- a/apps/jobs/models.py +++ b/apps/jobs/models.py @@ -1,3 +1,120 @@ from django.db import models # Create your models here. + +APPROVAL_STATUS_CHOICE = [ + ("draft", "Draft"), + ("pending", "Pending Review"), + ("active", "Active"), + ("closed", "Closed"), + ("rejected", "Rejected"), +] +EXPERIENCE_LEVEL_CHOICES = [ + ("entry", "Entry Level"), + ("mid", "Mid Level"), + ("senior", "Senior Level"), + ("executive", "Executive"), +] + +JOB_TYPE_CHOICES = [ + ("full_time", "Full Time"), + ("part_time", "Part Time"), + ("contract", "Contract"), + ("internship", "Internship"), + ("freelance", "Freelance"), +] + +JOB_APPLICATION_STATUS = [ + ("submitted", "Submitted"), + ("under_review", "Under Review"), + ("shortlisted", "Shortlisted"), + ("interview", "Interview"), + ("offered", "Offered"), + ("hired", "Hired"), + ("rejected", "Rejected"), + ("withdrawn", "Withdrawn"), +] + + +class Company(models.Model): + name = models.CharField(max_length=100) + logo = models.ImageField(upload_to="companies/", null=True, blank=True) + website = models.URLField(blank=True) + description = models.TextField(blank=True) + industry = models.CharField(max_length=100, blank=True) + size = models.CharField(max_length=50, blank=True) + location = models.CharField(max_length=100, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + self.name + + +class JobPosting(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + title = models.CharField(max_length=200) + company = models.ForeignKey( + Company, on_delete=models.CASCADE, related_name="job_postings" + ) + posted_by = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="posted_jobs" + ) + description = models.TextField() + requirements = models.TextField() + benefits = models.TextField(blank=True) + location = models.CharField(max_length=100) + is_remote = models.BooleanField(default=False) + job_type = models.CharField( + max_length=20, + choices=JOB_TYPE_CHOICES, + ) + experience_level = models.CharField(max_length=20, choices=EXPERIENCE_LEVEL_CHOICES) + salary_min = models.PositiveIntegerField(null=True, blank=True) + salary_max = models.PositiveIntegerField(null=True, blank=True) + salary_currency = models.CharField(max_length=3, default="USD") + application_deadline = models.DateTimeField(null=True, blank=True) + external_url = models.URLField(blank=True) + skills_required = models.JSONField(default=list) + status = models.CharField( + max_length=20, + choices=APPROVAL_STATUS_CHOICE, + default="pending", + ) + featured = models.BooleanField(default=False) + view_count = models.PositiveIntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + expires_at = models.DateTimeField() + + def __str__(self): + return self.title + + class Meta: + ordering = ["-created_at"] + indexes = [ + models.Index(fields=["title"]), + models.Index(fields=["company"]), + models.Index(fields=["status"]), + ] + + +class JobApplication(models.Model): + job = models.ForeignKey( + JobPosting, on_delete=models.CASCADE, related_name="applications" + ) + applicant = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="job_applications" + ) + cover_letter = models.TextField() + resume = models.FileField(upload_to="resumes/") + additional_documents = models.JSONField(default=list) + status = models.CharField( + max_length=20, + choices=JOB_APPLICATION_STATUS, + default="submitted", + ) + applied_at = models.DateTimeField(auto_now_add=True) + last_updated = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ["job", "applicant"] diff --git a/apps/jobs/serializers.py b/apps/jobs/serializers.py new file mode 100644 index 0000000..93f80d8 --- /dev/null +++ b/apps/jobs/serializers.py @@ -0,0 +1,119 @@ +from rest_framework import serializers +from jobs.models import Company, JobPosting, JobApplication +from apps.users.models import User + + +class CompanySerializer(serializers.ModelSerializer): + class Meta: + model = Company + fields = [ + "id", + "name", + "logo", + "website", + "description", + "industry", + "size", + "location", + "created_at", + ] + read_only_fields = ["id", "created_at"] + + +class PosterSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "username", "email"] + + +class JobPostingSerializer(serializers.ModelSerializer): + company = CompanySerializer(read_only=True) + company_id = serializers.PrimaryKeyRelatedField( + queryset=Company.objects.all(), source="company", write_only=True + ) + posted_by = PosterSerializer(read_only=True) + + class Meta: + model = JobPosting + fields = [ + "id", + "title", + "company", + "company_id", + "posted_by", + "description", + "requirements", + "benefits", + "location", + "is_remote", + "job_type", + "experience_level", + "salary_min", + "salary_max", + "salary_currency", + "application_deadline", + "external_url", + "skills_required", + "status", + "featured", + "view_count", + "created_at", + "updated_at", + "expires_at", + ] + read_only_fields = ["id", "created_at", "updated_at", "view_count"] + + +class JobPostingListSerializer(serializers.ModelSerializer): + company = CompanySerializer(read_only=True) + + class Meta: + model = JobPosting + fields = [ + "id", + "title", + "company", + "location", + "is_remote", + "job_type", + "experience_level", + "salary_min", + "salary_max", + "status", + "featured", + "expires_at", + ] + + +class ApplicantSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "username", "email"] + + +class JobApplicationSerializer(serializers.ModelSerializer): + applicant = ApplicantSerializer(read_only=True) + applicant_id = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), source="applicant", write_only=True, required=False + ) + job = JobPostingSerializer(read_only=True) + job_id = serializers.PrimaryKeyRelatedField( + queryset=JobPosting.objects.all(), source="job", write_only=True + ) + + class Meta: + model = JobApplication + fields = [ + "id", + "job", + "job_id", + "applicant", + "applicant_id", + "cover_letter", + "resume", + "additional_documents", + "status", + "applied_at", + "last_updated", + ] + read_only_fields = ["id", "applied_at", "last_updated", "status"] diff --git a/apps/jobs/urls.py b/apps/jobs/urls.py new file mode 100644 index 0000000..7142c6b --- /dev/null +++ b/apps/jobs/urls.py @@ -0,0 +1,9 @@ +from rest_framework.routers import DefaultRouter +from jobs.views import CompanyViewSet, JobPostingViewSet, JobApplicationViewSet + +router = DefaultRouter() +router.register(r"companies", CompanyViewSet) +router.register(r"jobs", JobPostingViewSet) +router.register(r"applications", JobApplicationViewSet) + +urlpatterns = router.urls diff --git a/apps/jobs/views.py b/apps/jobs/views.py index 91ea44a..21b1659 100644 --- a/apps/jobs/views.py +++ b/apps/jobs/views.py @@ -1,3 +1,66 @@ -from django.shortcuts import render +from jobs.models import Company, JobApplication, JobPosting +from jobs.serializers import ( + CompanySerializer, + JobApplicationSerializer, + JobPostingListSerializer, + JobPostingSerializer, +) +from rest_framework import permissions, viewsets +from rest_framework.exceptions import PermissionDenied -# Create your views here. + +class CompanyViewSet(viewsets.ModelViewSet): + queryset = Company.objects.all() + serializer_class = CompanySerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def perform_create(self, serializer): + serializer.save() + + +class JobPostingViewSet(viewsets.ModelViewSet): + queryset = JobPosting.objects.all().select_related("company", "posted_by") + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get_serializer_class(self): + if self.action == "list": + return JobPostingListSerializer + return JobPostingSerializer + + def perform_create(self, serializer): + serializer.save(posted_by=self.request.user) + + def perform_update(self, serializer): + if serializer.instance.posted_by != self.request.user: + raise PermissionDenied("You can only update jobs you've posted.") + serializer.save() + + def perform_destroy(self, instance): + if instance.posted_by != self.request.user: + raise PermissionDenied("You can only delete jobs you've posted.") + instance.delete() + + +class JobApplicationViewSet(viewsets.ModelViewSet): + queryset = JobApplication.objects.select_related("job", "applicant").all() + serializer_class = JobApplicationSerializer + permission_classes = [permissions.IsAuthenticated] + + def perform_create(self, serializer): + job = serializer.validated_data["job"] + applicant = self.request.user + + if JobApplication.objects.filter(job=job, applicant=applicant).exists(): + raise PermissionDenied("You have already applied for this job.") + + serializer.save(applicant=applicant) + + def perform_update(self, serializer): + if serializer.instance.applicant != self.request.user: + raise PermissionDenied("You can only modify your own applications.") + serializer.save() + + def perform_destroy(self, instance): + if instance.applicant != self.request.user: + raise PermissionDenied("You can only delete your own applications.") + instance.delete()