diff --git a/jobs/admin.py b/jobs/admin.py index 0881a2a..e669d97 100644 --- a/jobs/admin.py +++ b/jobs/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Job +from .models import Job, Tag @admin.register(Job) class JobAdmin(admin.ModelAdmin): @@ -7,4 +7,8 @@ class JobAdmin(admin.ModelAdmin): list_filter = ('is_remote', 'salary_range') search_fields = ('title', 'company_name', 'location') +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + search_fields = ('name',) + admin.site.site_header = "PythonPH Jobs Board" diff --git a/jobs/management/commands/populate_jobs.py b/jobs/management/commands/populate_jobs.py index fa86f0f..6f243b6 100644 --- a/jobs/management/commands/populate_jobs.py +++ b/jobs/management/commands/populate_jobs.py @@ -1,5 +1,5 @@ from django.core.management.base import BaseCommand -from jobs.models import Job +from jobs.models import Job, Tag from faker import Faker import random @@ -19,8 +19,9 @@ def handle(self, *args, **kwargs): fake = Faker() count = kwargs['count'] - # Delete all existing jobs + # Delete all existing jobs and tags Job.objects.all().delete() + Tag.objects.all().delete() # Common tech job titles and skills for more realistic data tech_roles = [ @@ -38,6 +39,15 @@ def handle(self, *args, **kwargs): 'Davao City', 'Pasig', 'Mandaluyong', 'Pasay' ] + # Create tags + tags = [ + Tag(name='Django'), + Tag(name='FastAPI'), + Tag(name='Pandas'), + Tag(name='Automation') + ] + Tag.objects.bulk_create(tags) + # Create jobs for _ in range(count): # Generate salary range (in thousands) @@ -66,6 +76,8 @@ def handle(self, *args, **kwargs): is_remote=random.choice([True, False, True]) # 66% chance of remote work ) + job.assigned_tags.add(*random.sample(tags, random.choice(range(1, 3)))) # add 1 or 2 tags + self.stdout.write( self.style.SUCCESS(f'Created job: {job.title} at {job.company_name}') ) diff --git a/jobs/migrations/0003_tag_job_assigned_tags.py b/jobs/migrations/0003_tag_job_assigned_tags.py new file mode 100644 index 0000000..6db2bc7 --- /dev/null +++ b/jobs/migrations/0003_tag_job_assigned_tags.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.6 on 2026-04-04 19:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('jobs', '0002_job_short_description'), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ], + ), + migrations.AddField( + model_name='job', + name='assigned_tags', + field=models.ManyToManyField(blank=True, to='jobs.tag'), + ), + ] diff --git a/jobs/models.py b/jobs/models.py index 4be71dd..9ed96db 100644 --- a/jobs/models.py +++ b/jobs/models.py @@ -1,6 +1,21 @@ from django.db import models +from django.db.models.functions import Lower +class Tag(models.Model): + name = models.CharField(max_length=100, unique=True) + + def __str__(self): + return self.name + + class Meta: + constraints = [ + models.UniqueConstraint( + Lower('name'), + name='case insensitive uniqueness check' + ), + ] + class Job(models.Model): title = models.CharField(max_length=200) company_name = models.CharField(max_length=200) @@ -11,6 +26,7 @@ class Job(models.Model): is_remote = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + assigned_tags = models.ManyToManyField(Tag, blank=True) def __str__(self): return f"{self.title} at {self.company_name}" diff --git a/jobs/templates/content.html b/jobs/templates/content.html index 3403824..c8b46f0 100644 --- a/jobs/templates/content.html +++ b/jobs/templates/content.html @@ -26,6 +26,11 @@ + +{% if tag_query %} +
Showing jobs tagged '{{ tag_query }}':
+{% endif %} +
{% for job in object_list %} @@ -43,6 +48,11 @@
Remote
{% endif %}
{{ job.salary_range }}
+ {% for tag in job.tags %} +
+ {{ tag }} +
+ {% endfor %}

{{ job.short_description }} diff --git a/jobs/templates/job_detail.html b/jobs/templates/job_detail.html index e736070..06ba1e8 100644 --- a/jobs/templates/job_detail.html +++ b/jobs/templates/job_detail.html @@ -18,6 +18,11 @@

{{ job.company_name }}

Remote
{% endif %}
{{ job.salary_range }}
+ {% for tag in job.tags %} +
+ {{ tag }} +
+ {% endfor %} diff --git a/jobs/templates/pagination.html b/jobs/templates/pagination.html index a58f0be..36119a8 100644 --- a/jobs/templates/pagination.html +++ b/jobs/templates/pagination.html @@ -2,10 +2,10 @@ diff --git a/jobs/views.py b/jobs/views.py index 63e4dec..23aea4a 100644 --- a/jobs/views.py +++ b/jobs/views.py @@ -1,6 +1,7 @@ from django.views.generic import ListView, DetailView -from django.db.models import Q, Count, Max -from .models import Job +from django.db.models import Q, Count, Max, OuterRef +from django.contrib.postgres.expressions import ArraySubquery +from .models import Job, Tag class JobIndex(ListView): @@ -9,13 +10,20 @@ class JobIndex(ListView): paginate_by = 10 def get_queryset(self): - queryset = super().get_queryset() + queryset = super().get_queryset().annotate( + tags = ArraySubquery(Tag.objects.filter(job=OuterRef('pk')).values('name')) + ) search_query = self.request.GET.get('search', '') + tag_query = self.request.GET.get('tag', '') if search_query: queryset = queryset.filter( Q(title__icontains=search_query) | Q(company_name__icontains=search_query) ) + if tag_query: + queryset = queryset.filter( + tags__contains=[tag_query] + ) return queryset def get_context_data(self, **kwargs): @@ -23,6 +31,7 @@ def get_context_data(self, **kwargs): context["job_count"] = self.get_queryset().count() context["company_count"] = Job.objects.values('company_name').distinct().count() context["search_query"] = self.request.GET.get('search', '') + context["tag_query"] = self.request.GET.get('tag', '') context["description"] = 'Python.PH Jobs Board' context["page_type"] = 'website' return context @@ -32,6 +41,11 @@ class JobDetail(DetailView): template_name = "job_detail.html" context_object_name = 'job' + def get_queryset(self): + return super().get_queryset().annotate( + tags = ArraySubquery(Tag.objects.filter(job=OuterRef('pk')).values('name')) + ) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Add related jobs from the same company