diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..7ccbb78d Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 02605806..fc2708c0 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,6 @@ dmypy.json .idea/workspace.xml # Exception for Tailwind CSS -!theme/static/css/dist/ \ No newline at end of file +!theme/static/css/dist/ + +**/.DS_Store diff --git a/jobs/__init__.py b/jobs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/jobs/admin.py b/jobs/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/jobs/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/jobs/apps.py b/jobs/apps.py new file mode 100644 index 00000000..241a7f1b --- /dev/null +++ b/jobs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class JobsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "jobs" diff --git a/jobs/forms.py b/jobs/forms.py new file mode 100644 index 00000000..0dff4ebe --- /dev/null +++ b/jobs/forms.py @@ -0,0 +1,33 @@ +from django import forms +from django.core.exceptions import ValidationError + +from .models import Job + + +class JobForm(forms.ModelForm): + class Meta: + model = Job + fields = [ + "title", + "description", + "sector", + "contract_type", + "pay", + "employer_name", + "incorporation_number", + "website", + "expiry_date", + "application_url", + ] + + def clean_expiry_date(self): + expiry_date = self.cleaned_data.get("expiry_date") + + # Check if the expiry_date is in a valid format + if not expiry_date: + raise ValidationError("Please enter a valid date.") + + # Custom validation logic for the expiry_date field + # ... + + return expiry_date diff --git a/jobs/management/__init__.py b/jobs/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/jobs/management/commands/__init__.py b/jobs/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/jobs/management/commands/unpublish_listings.py b/jobs/management/commands/unpublish_listings.py new file mode 100644 index 00000000..277f551b --- /dev/null +++ b/jobs/management/commands/unpublish_listings.py @@ -0,0 +1,12 @@ +import datetime +from django.core.management.base import BaseCommand +from jobs.models import Job + + +class Command(BaseCommand): + help = "Unpublish expired job listings" + + def handle(self, *args, **options): + expired_date = datetime.date.today() - datetime.timedelta(days=30) + expired_listings = Job.objects.filter(created__lt=expired_date) + expired_listings.update(is_published=False) diff --git a/jobs/migrations/0001_initial.py b/jobs/migrations/0001_initial.py new file mode 100644 index 00000000..c002e748 --- /dev/null +++ b/jobs/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.19 on 2023-05-20 13:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Job", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=100)), + ("description", models.TextField()), + ( + "sector", + models.CharField( + choices=[("commercial", "Commercial"), ("charity", "Charity")], + max_length=20, + ), + ), + ( + "pay", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ("employer_name", models.CharField(max_length=100)), + ( + "incorporation_number", + models.CharField(blank=True, max_length=20, null=True), + ), + ("website", models.URLField()), + ("expiry_date", models.DateField()), + ("application_url", models.URLField()), + ], + ), + ] diff --git a/jobs/migrations/0002_job_contract_type.py b/jobs/migrations/0002_job_contract_type.py new file mode 100644 index 00000000..f3ba4916 --- /dev/null +++ b/jobs/migrations/0002_job_contract_type.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.19 on 2023-05-20 13:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="job", + name="contract_type", + field=models.CharField( + choices=[ + ("voluntary", "Voluntary (Unpaid)"), + ("temporary", "Temporary"), + ("fixed_term_contract", "Fixed Term Contract"), + ("part_time_permanent", "Part-time Permanent Employed"), + ("full_time_permanent", "Full-time Permanent Employed"), + ], + default="", + max_length=20, + ), + preserve_default=False, + ), + ] diff --git a/jobs/migrations/0003_auto_20230610_0843.py b/jobs/migrations/0003_auto_20230610_0843.py new file mode 100644 index 00000000..81f592df --- /dev/null +++ b/jobs/migrations/0003_auto_20230610_0843.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.19 on 2023-06-10 08:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0002_job_contract_type"), + ] + + operations = [ + migrations.AlterField( + model_name="job", + name="contract_type", + field=models.CharField( + choices=[ + ("Voluntary", "Voluntary (Unpaid)"), + ("Temporary", "Temporary"), + ("Fixed Term Contract", "Fixed Term Contract"), + ("Part-time Permanent", "Part Time Permanent Employed"), + ("Full-time Permanent Employed", "Full-time Permanent Employed"), + ], + max_length=40, + ), + ), + migrations.AlterField( + model_name="job", + name="sector", + field=models.CharField( + choices=[("Commercial", "Commercial"), ("Charity", "Charity")], + max_length=20, + ), + ), + ] diff --git a/jobs/migrations/0004_auto_20230610_0852.py b/jobs/migrations/0004_auto_20230610_0852.py new file mode 100644 index 00000000..a022bc94 --- /dev/null +++ b/jobs/migrations/0004_auto_20230610_0852.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.19 on 2023-06-10 08:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0003_auto_20230610_0843"), + ] + + operations = [ + migrations.AlterField( + model_name="job", + name="contract_type", + field=models.CharField( + choices=[ + ("voluntary", "Voluntary"), + ("temporary", "Temporary"), + ("fixed_term_contract", "Fixed Term Contract"), + ("part_time_permanent", "Part-time Permanent"), + ("full_time_permanent", "Full-time Permanent Employed"), + ], + max_length=40, + ), + ), + migrations.AlterField( + model_name="job", + name="sector", + field=models.CharField( + choices=[("commercial", "Commercial"), ("charity", "Charity")], + max_length=20, + ), + ), + ] diff --git a/jobs/migrations/0005_auto_20230610_0901.py b/jobs/migrations/0005_auto_20230610_0901.py new file mode 100644 index 00000000..05d639ab --- /dev/null +++ b/jobs/migrations/0005_auto_20230610_0901.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.19 on 2023-06-10 09:01 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0004_auto_20230610_0852"), + ] + + operations = [ + migrations.AlterModelOptions( + name="job", + options={"ordering": ["-created"]}, + ), + migrations.AddField( + model_name="job", + name="created", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + ] diff --git a/jobs/migrations/0006_job_is_published.py b/jobs/migrations/0006_job_is_published.py new file mode 100644 index 00000000..4a997025 --- /dev/null +++ b/jobs/migrations/0006_job_is_published.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.19 on 2023-06-26 18:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0005_auto_20230610_0901"), + ] + + operations = [ + migrations.AddField( + model_name="job", + name="is_published", + field=models.BooleanField(default=True), + ), + ] diff --git a/jobs/migrations/0007_job_published_date.py b/jobs/migrations/0007_job_published_date.py new file mode 100644 index 00000000..a69af9f6 --- /dev/null +++ b/jobs/migrations/0007_job_published_date.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.19 on 2023-06-26 19:05 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0006_job_is_published"), + ] + + operations = [ + migrations.AddField( + model_name="job", + name="published_date", + field=models.DateField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + ] diff --git a/jobs/migrations/0008_auto_20230626_1917.py b/jobs/migrations/0008_auto_20230626_1917.py new file mode 100644 index 00000000..9ad92d49 --- /dev/null +++ b/jobs/migrations/0008_auto_20230626_1917.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.19 on 2023-06-26 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0007_job_published_date"), + ] + + operations = [ + migrations.RemoveField( + model_name="job", + name="published_date", + ), + migrations.AddField( + model_name="job", + name="updated", + field=models.DateField(auto_now=True), + ), + ] diff --git a/jobs/migrations/0009_auto_20230626_1922.py b/jobs/migrations/0009_auto_20230626_1922.py new file mode 100644 index 00000000..0b433763 --- /dev/null +++ b/jobs/migrations/0009_auto_20230626_1922.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.19 on 2023-06-26 19:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0008_auto_20230626_1917"), + ] + + operations = [ + migrations.RemoveField( + model_name="job", + name="updated", + ), + migrations.AddField( + model_name="job", + name="updated_on", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/jobs/migrations/0010_auto_20230626_1924.py b/jobs/migrations/0010_auto_20230626_1924.py new file mode 100644 index 00000000..72b5825e --- /dev/null +++ b/jobs/migrations/0010_auto_20230626_1924.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.19 on 2023-06-26 19:24 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0009_auto_20230626_1922"), + ] + + operations = [ + migrations.RemoveField( + model_name="job", + name="updated_on", + ), + migrations.AddField( + model_name="job", + name="published_date", + field=models.DateField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + ] diff --git a/jobs/migrations/0011_alter_job_incorporation_number.py b/jobs/migrations/0011_alter_job_incorporation_number.py new file mode 100644 index 00000000..e3542898 --- /dev/null +++ b/jobs/migrations/0011_alter_job_incorporation_number.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.22 on 2023-10-08 13:27 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("jobs", "0010_auto_20230626_1924"), + ] + + operations = [ + migrations.AlterField( + model_name="job", + name="incorporation_number", + field=models.CharField(blank=True, default="", max_length=20), + preserve_default=False, + ), + ] diff --git a/jobs/migrations/__init__.py b/jobs/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/jobs/models.py b/jobs/models.py new file mode 100644 index 00000000..7246b0ab --- /dev/null +++ b/jobs/models.py @@ -0,0 +1,42 @@ +from django.db import models +from django.urls import reverse + + +# Create your models here. + + +class Job(models.Model): + SECTOR_CHOICES = [("commercial", "Commercial"), ("charity", "Charity")] + + CONTRACT_TYPE_CHOICES = [ + ("voluntary", "Voluntary"), + ("temporary", "Temporary"), + ("fixed_term_contract", "Fixed Term Contract"), + ("part_time_permanent", "Part-time Permanent"), + ("full_time_permanent", "Full-time Permanent Employed"), + ] + + title = models.CharField(max_length=100) + description = models.TextField() + sector = models.CharField(max_length=20, choices=SECTOR_CHOICES) + contract_type = models.CharField(max_length=40, choices=CONTRACT_TYPE_CHOICES) + pay = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + employer_name = models.CharField(max_length=100) + incorporation_number = models.CharField(max_length=20, blank=True) + website = models.URLField(max_length=200) + expiry_date = models.DateField() + application_url = models.URLField(max_length=200) + is_published = models.BooleanField(default=True) + created = models.DateTimeField(auto_now_add=True) + published_date = models.DateField(auto_now_add=True) + + class Meta: + ordering = ["-created"] + + def is_expired(self): + from datetime import date + + return date.today() > self.expiry_date + + def get_absolute_url(self): + return reverse("jobs:job_detail", kwargs={"pk": self.pk}) diff --git a/jobs/templates/jobs/create_job.html b/jobs/templates/jobs/create_job.html new file mode 100644 index 00000000..632d82eb --- /dev/null +++ b/jobs/templates/jobs/create_job.html @@ -0,0 +1,191 @@ +{% extends 'base.html' %} + +{% block content %} + + {% if form.errors %} + {{ form.errors }} + {% endif %} + +
+
+

Post Job

+
+ {% csrf_token %} +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/jobs/templates/jobs/job_detail.html b/jobs/templates/jobs/job_detail.html new file mode 100644 index 00000000..3299dc61 --- /dev/null +++ b/jobs/templates/jobs/job_detail.html @@ -0,0 +1,60 @@ +{% extends 'base.html' %} + +{% block content %} + +
+
+

{{ job.title }}

+
+
+
+
+
Job Description
+
{{ job.description }} +
+
+
+
Sector
+
{{ job.sector }}
+
+
+
Contract Type
+
{{ job.contract_type }}
+
+
+
Pay
+
£{{ job.pay }}
+
+
+
Employer
+
{{ job.employer_name }}
+
+
+
Incorporation Number
+
{{ job.incorporation_number }}
+
+ +
+
Expiry Date
+
{{ job.expiry_date }}
+
+
+
Application link:
+
+ Apply +
+
+ + + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/jobs/templates/jobs/job_listing.html b/jobs/templates/jobs/job_listing.html new file mode 100644 index 00000000..af0913d3 --- /dev/null +++ b/jobs/templates/jobs/job_listing.html @@ -0,0 +1,71 @@ +{% extends 'base.html' %} + +{% block content %} + + + + +
+ + +
+ + + + +{% endblock %} diff --git a/jobs/tests.py b/jobs/tests.py new file mode 100644 index 00000000..86784d7f --- /dev/null +++ b/jobs/tests.py @@ -0,0 +1,40 @@ +from django.test import TestCase +from django.urls import reverse + +from .models import Job + +# Create your tests here. + + +class JobTestCase(TestCase): + def setUp(self): + self.job = Job.objects.create( + title="New Job", + description="Job 1", + sector="commercial", + contract_type="temporary", + pay=40, + employer_name="Google", + incorporation_number="jdj343", + website="https:www.abc.abc", + expiry_date="2023-06-30", + application_url="https://www.abc.abc", + is_published=True, + ) + + def test_job_list_view(self): + response = self.client.get(reverse("jobs:job_listing")) + self.assertEqual(response.status_code, 200) + + def test_job_count(self): + jobs = Job.objects.all().count() + self.assertEqual(jobs, 1) + + def test_job_id(self): + job = Job.objects.get(pk=self.job.pk) + self.assertEqual(job.title, "New Job") + + def test_get_absolute_url(self): + job = Job.objects.get(pk=self.job.pk) + expected_url = job.get_absolute_url() + self.assertEqual(expected_url, f"/jobs/{job.pk}/") diff --git a/jobs/urls.py b/jobs/urls.py new file mode 100644 index 00000000..48123395 --- /dev/null +++ b/jobs/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +app_name = "jobs" + +urlpatterns = [ + path("", views.job_listing, name="job_listing"), + path("create/", views.create_job, name="create_job"), + path("/", views.job_detail, name="job_detail"), +] diff --git a/jobs/views.py b/jobs/views.py new file mode 100644 index 00000000..c15390ae --- /dev/null +++ b/jobs/views.py @@ -0,0 +1,39 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.core.paginator import Paginator +from django.views.decorators.http import require_http_methods +from django.urls import reverse +from django.views.decorators.http import require_POST, require_GET + + +from .models import Job +from .forms import JobForm + +# Create your views here. + + +@require_GET +@require_POST +def create_job(request): + form = JobForm(request.POST or None) + + if request.method == "POST" and form.is_valid(): + form.save() + return redirect(reverse("jobs:job_listing")) + + return render(request, "jobs/create_job.html", {"form": form}) + + +@require_http_methods(["GET"]) +def job_listing(request): + jobs = Job.objects.filter(is_published=True) + paginator = Paginator(jobs, 10) + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) + return render(request, "jobs/job_listing.html", {"jobs": page_obj}) + + +@require_http_methods(["GET"]) +def job_detail(request, pk): + job = get_object_or_404(Job, pk=pk) + + return render(request, "jobs/job_detail.html", {"job": job}) diff --git a/memberships/migrations/0015_auto_20231008_1327.py b/memberships/migrations/0015_auto_20231008_1327.py new file mode 100644 index 00000000..2f451b0d --- /dev/null +++ b/memberships/migrations/0015_auto_20231008_1327.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.22 on 2023-10-08 13:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("memberships", "0014_merge_20210421_1702"), + ] + + operations = [ + migrations.AlterField( + model_name="failedpayment", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="member", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="membership", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="payment", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/web/settings.py b/web/settings.py index f8a1c9ec..74be4b78 100644 --- a/web/settings.py +++ b/web/settings.py @@ -54,6 +54,7 @@ "memberships", "django_extensions", "django_probes", + "jobs", ] MIDDLEWARE = [ diff --git a/web/urls.py b/web/urls.py index f793ea30..f32631bb 100644 --- a/web/urls.py +++ b/web/urls.py @@ -21,4 +21,5 @@ path("admin/", admin.site.urls), path("memberships/", include("memberships.urls")), path("theme/", include("theme.urls")), + path("jobs/", include("jobs.urls")), ]