diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a..1741cb2 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..eca0993 Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5f..4e53e6a 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988..87c7640 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c..1999a5f 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..ba6d035 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,18 @@ from django.contrib import admin -# Register your models here. +from .models import JobPosting, JobSource + + +@admin.register(JobSource) +class JobSourceAdmin(admin.ModelAdmin): + list_display = ("name", "family", "status", "url", "last_checked_at", "created_at") + list_filter = ("family", "status") + search_fields = ("name", "url", "owner") + + +@admin.register(JobPosting) +class JobPostingAdmin(admin.ModelAdmin): + list_display = ("title", "company", "location", "contract_type", "source", "published_at", "is_active") + list_filter = ("contract_type", "is_active", "source__family", "published_at") + search_fields = ("title", "company", "location", "description") + autocomplete_fields = ("source",) diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..b4267ed --- /dev/null +++ b/core/forms.py @@ -0,0 +1,69 @@ +from django import forms + +from .models import JobPosting, JobSource + + +class StyledModelForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + widget = field.widget + if isinstance(widget, forms.CheckboxInput): + widget.attrs.setdefault("class", "form-check-input") + elif isinstance(widget, forms.Select): + widget.attrs.setdefault("class", "form-select") + else: + widget.attrs.setdefault("class", "form-control") + + +class JobSourceForm(StyledModelForm): + class Meta: + model = JobSource + fields = ["name", "family", "url", "status", "owner", "notes"] + widgets = { + "notes": forms.Textarea(attrs={"rows": 4}), + } + help_texts = { + "url": "Paste the public job board, agency, or careers page URL.", + "owner": "Optional teammate responsible for this connector.", + } + + def clean_name(self): + return self.cleaned_data["name"].strip() + + +class JobPostingForm(StyledModelForm): + class Meta: + model = JobPosting + fields = [ + "source", + "title", + "company", + "location", + "contract_type", + "remote", + "salary", + "apply_url", + "published_at", + "description", + "is_active", + ] + widgets = { + "published_at": forms.DateInput(attrs={"type": "date"}), + "description": forms.Textarea(attrs={"rows": 6}), + } + help_texts = { + "source": "Choose the connector/source that found this offer.", + "description": "Paste a concise cleaned description; the pipeline view will evolve from here.", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["source"].queryset = JobSource.objects.order_by("family", "name") + self.fields["source"].empty_label = "Select a source" + + def clean_title(self): + return self.cleaned_data["title"].strip() + + def clean_company(self): + return self.cleaned_data["company"].strip() diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..d155908 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 5.2.7 on 2026-06-09 22:57 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='JobSource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=160)), + ('family', models.CharField(choices=[('portal', 'National portal'), ('agency', 'Interim agency'), ('company', 'Company careers')], max_length=24)), + ('url', models.URLField(unique=True)), + ('status', models.CharField(choices=[('planned', 'Planned'), ('active', 'Active'), ('paused', 'Paused'), ('error', 'Needs attention')], default='planned', max_length=24)), + ('owner', models.CharField(blank=True, max_length=120)), + ('notes', models.TextField(blank=True)), + ('last_checked_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['family', 'name'], + 'indexes': [models.Index(fields=['family', 'status'], name='core_jobsou_family_d61233_idx'), models.Index(fields=['name'], name='core_jobsou_name_a1bfb5_idx')], + }, + ), + migrations.CreateModel( + name='JobPosting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=220)), + ('company', models.CharField(max_length=180)), + ('location', models.CharField(default='Dijon, Bourgogne-Franche-Comté', max_length=160)), + ('contract_type', models.CharField(choices=[('cdi', 'CDI'), ('cdd', 'CDD'), ('interim', 'Interim'), ('apprenticeship', 'Apprenticeship'), ('freelance', 'Freelance'), ('other', 'Other')], default='cdi', max_length=24)), + ('remote', models.BooleanField(default=False)), + ('salary', models.CharField(blank=True, max_length=120)), + ('apply_url', models.URLField(blank=True)), + ('published_at', models.DateField(default=django.utils.timezone.localdate)), + ('description', models.TextField()), + ('is_active', models.BooleanField(default=True)), + ('duplicate_score', models.DecimalField(decimal_places=2, default=0, max_digits=5)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('source', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='jobs', to='core.jobsource')), + ], + options={ + 'ordering': ['-published_at', '-created_at'], + 'indexes': [models.Index(fields=['is_active', 'published_at'], name='core_jobpos_is_acti_ee6ffc_idx'), models.Index(fields=['company', 'title'], name='core_jobpos_company_49594a_idx'), models.Index(fields=['contract_type'], name='core_jobpos_contrac_b2fa07_idx')], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..e13f837 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..a13a58b 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,78 @@ from django.db import models +from django.urls import reverse +from django.utils import timezone -# Create your models here. + +class JobSource(models.Model): + class Family(models.TextChoices): + PORTAL = "portal", "National portal" + AGENCY = "agency", "Interim agency" + COMPANY = "company", "Company careers" + + class Status(models.TextChoices): + PLANNED = "planned", "Planned" + ACTIVE = "active", "Active" + PAUSED = "paused", "Paused" + ERROR = "error", "Needs attention" + + name = models.CharField(max_length=160) + family = models.CharField(max_length=24, choices=Family.choices) + url = models.URLField(unique=True) + status = models.CharField(max_length=24, choices=Status.choices, default=Status.PLANNED) + owner = models.CharField(max_length=120, blank=True) + notes = models.TextField(blank=True) + last_checked_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["family", "name"] + indexes = [ + models.Index(fields=["family", "status"]), + models.Index(fields=["name"]), + ] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse("source_success", args=[self.pk]) + + +class JobPosting(models.Model): + class ContractType(models.TextChoices): + CDI = "cdi", "CDI" + CDD = "cdd", "CDD" + INTERIM = "interim", "Interim" + APPRENTICESHIP = "apprenticeship", "Apprenticeship" + FREELANCE = "freelance", "Freelance" + OTHER = "other", "Other" + + source = models.ForeignKey(JobSource, on_delete=models.PROTECT, related_name="jobs") + title = models.CharField(max_length=220) + company = models.CharField(max_length=180) + location = models.CharField(max_length=160, default="Dijon, Bourgogne-Franche-Comté") + contract_type = models.CharField(max_length=24, choices=ContractType.choices, default=ContractType.CDI) + remote = models.BooleanField(default=False) + salary = models.CharField(max_length=120, blank=True) + apply_url = models.URLField(blank=True) + published_at = models.DateField(default=timezone.localdate) + description = models.TextField() + is_active = models.BooleanField(default=True) + duplicate_score = models.DecimalField(max_digits=5, decimal_places=2, default=0) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-published_at", "-created_at"] + indexes = [ + models.Index(fields=["is_active", "published_at"]), + models.Index(fields=["company", "title"]), + models.Index(fields=["contract_type"]), + ] + + def __str__(self): + return f"{self.title} — {self.company}" + + def get_absolute_url(self): + return reverse("job_detail", args=[self.pk]) diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..98dbb73 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -3,23 +3,68 @@
-