diff --git a/backend/api/job_board/schema.py b/backend/api/job_board/schema.py
index e58be29429..95c3da458a 100644
--- a/backend/api/job_board/schema.py
+++ b/backend/api/job_board/schema.py
@@ -11,7 +11,9 @@ class JobBoardQuery:
def job_listings(self, conference: str) -> list[JobListingType]:
return [
JobListingType.from_django_model(listing)
- for listing in JobListing.objects.filter(conference__code=conference).all()
+ for listing in JobListing.objects.filter(conference__code=conference)
+ .order_by("order")
+ .all()
]
@strawberry.field
diff --git a/backend/api/job_board/types.py b/backend/api/job_board/types.py
index cde24a9f95..c0ca32a654 100644
--- a/backend/api/job_board/types.py
+++ b/backend/api/job_board/types.py
@@ -2,15 +2,13 @@
import strawberry
-from ..helpers.i18n import make_localized_resolver
-
@strawberry.type
class JobListing:
id: strawberry.ID
- title: str = strawberry.field(resolver=make_localized_resolver("title"))
- slug: str = strawberry.field(resolver=make_localized_resolver("slug"))
- description: str = strawberry.field(resolver=make_localized_resolver("description"))
+ title: str
+ slug: str
+ description: str
company: str
company_logo_url: strawberry.Private[Optional[str]]
apply_url: str
diff --git a/backend/api/tests/test_job_board.py b/backend/api/tests/test_job_board.py
index cecbf9b7b9..6e875ed306 100644
--- a/backend/api/tests/test_job_board.py
+++ b/backend/api/tests/test_job_board.py
@@ -3,7 +3,6 @@
from pytest import mark
from helpers.tests import get_image_url_from_request
-from i18n.strings import LazyI18nString
def _query_job_board(client, conference):
@@ -50,7 +49,7 @@ def test_query_job_board(rf, graphql_client):
@mark.django_db
def test_query_single_job_listing(rf, graphql_client):
listing = JobListingFactory(
- slug=LazyI18nString({"en": "demo", "it": "esempio"}),
+ slug="demo",
company_logo=None,
)
@@ -92,15 +91,15 @@ def test_query_single_job_listing(rf, graphql_client):
@mark.django_db
def test_passing_language(graphql_client):
JobListingFactory(
- title=LazyI18nString({"en": "this is a test", "it": "diventa una lumaca"}),
- slug=LazyI18nString({"en": "slug", "it": "lumaca"}),
+ title="diventa una lumaca",
+ slug="lumaca",
)
resp = graphql_client.query(
"""query {
- jobListing(slug: "slug") {
- title(language: "it")
- slug(language: "it")
+ jobListing(slug: "lumaca") {
+ title
+ slug
}
} """
)
diff --git a/backend/cms/components/page/tasks.py b/backend/cms/components/page/tasks.py
index b090541d2b..2c92d43da8 100644
--- a/backend/cms/components/page/tasks.py
+++ b/backend/cms/components/page/tasks.py
@@ -14,7 +14,6 @@ def revalidate_vercel_frontend_task(page_id):
settings = VercelFrontendSettings.for_site(site)
- site_name = site.site_name
hostname = site.hostname
url = settings.revalidate_url
@@ -48,6 +47,15 @@ def revalidate_vercel_frontend_task(page_id):
else:
path = f"/{language_code}{page_path}"
+ execute_frontend_revalidate(
+ url=url,
+ path=path,
+ secret=secret,
+ )
+
+
+@app.task
+def execute_frontend_revalidate(url: str, path: str, secret: str):
try:
response = requests.post(
url,
@@ -59,7 +67,7 @@ def revalidate_vercel_frontend_task(page_id):
)
response.raise_for_status()
except Exception as e:
- logger.error(f"Error while revalidating {path} on {site_name}: {e}")
+ logger.error(f"Error while revalidating {path}: {e}")
return
- logger.info(f"Revalidated {path} on {site_name}")
+ logger.info(f"Revalidated {path}")
diff --git a/backend/conferences/admin/conference.py b/backend/conferences/admin/conference.py
index 3340090f82..3244a302d7 100644
--- a/backend/conferences/admin/conference.py
+++ b/backend/conferences/admin/conference.py
@@ -162,6 +162,15 @@ class ConferenceAdmin(
)
},
),
+ (
+ "Frontend Settings",
+ {
+ "fields": (
+ "frontend_revalidate_url",
+ "frontend_revalidate_secret",
+ )
+ },
+ ),
(
"Conference",
{
diff --git a/backend/conferences/migrations/0054_conference_frontend_revalidate_secret_and_more.py b/backend/conferences/migrations/0054_conference_frontend_revalidate_secret_and_more.py
new file mode 100644
index 0000000000..a07b885bd0
--- /dev/null
+++ b/backend/conferences/migrations/0054_conference_frontend_revalidate_secret_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.4 on 2025-04-13 18:06
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('conferences', '0053_conference_slack_new_invitation_letter_request_channel_id'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='conference',
+ name='frontend_revalidate_secret',
+ field=models.CharField(blank=True, default='', max_length=32224),
+ ),
+ migrations.AddField(
+ model_name='conference',
+ name='frontend_revalidate_url',
+ field=models.URLField(blank=True, default=''),
+ ),
+ ]
diff --git a/backend/conferences/models/conference.py b/backend/conferences/models/conference.py
index 1d9aa5ea62..e04db0665a 100644
--- a/backend/conferences/models/conference.py
+++ b/backend/conferences/models/conference.py
@@ -148,6 +148,16 @@ class Conference(GeoLocalizedModel, TimeFramedModel, TimeStampedModel):
blank=True,
)
+ frontend_revalidate_url = models.URLField(
+ default="",
+ blank=True,
+ )
+ frontend_revalidate_secret = models.CharField(
+ default="",
+ blank=True,
+ max_length=32224,
+ )
+
def get_slack_oauth_token(self):
return self.organizer.slack_oauth_bot_token
diff --git a/backend/job_board/admin.py b/backend/job_board/admin.py
index d27d66998f..1d8fe640b6 100644
--- a/backend/job_board/admin.py
+++ b/backend/job_board/admin.py
@@ -1,5 +1,7 @@
from django.contrib import admin
+from cms.components.page.tasks import execute_frontend_revalidate
from ordered_model.admin import OrderedModelAdmin
+from custom_admin.widgets import RichEditorWidget
from .models import JobListing
@@ -9,3 +11,28 @@ class JobListingAdmin(OrderedModelAdmin):
model = JobListing
list_display = ("title", "company", "conference", "move_up_down_links")
list_filter = ("conference",)
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name == "description":
+ kwargs["widget"] = RichEditorWidget()
+
+ return super().formfield_for_dbfield(db_field, **kwargs)
+
+ def save_model(self, request, obj, form, change):
+ super().save_model(request, obj, form, change)
+ conference = obj.conference
+
+ if not conference.frontend_revalidate_url:
+ return
+
+ for locale in ["en", "it"]:
+ execute_frontend_revalidate.delay(
+ url=conference.frontend_revalidate_url,
+ path=f"/{locale}/jobs/",
+ secret=conference.frontend_revalidate_secret,
+ )
+ execute_frontend_revalidate.delay(
+ url=conference.frontend_revalidate_url,
+ path=f"/{locale}/jobs/{obj.id}",
+ secret=conference.frontend_revalidate_secret,
+ )
diff --git a/backend/job_board/migrations/0006_alter_joblisting_apply_url_and_more.py b/backend/job_board/migrations/0006_alter_joblisting_apply_url_and_more.py
new file mode 100644
index 0000000000..d9a987f32d
--- /dev/null
+++ b/backend/job_board/migrations/0006_alter_joblisting_apply_url_and_more.py
@@ -0,0 +1,51 @@
+# Generated by Django 5.1.4 on 2025-04-13 16:00
+
+from django.db import migrations, models
+
+
+def fix_data(apps, schema_editor):
+ JobListing = apps.get_model('job_board', 'JobListing')
+ for job_listing in JobListing.objects.all():
+ if job_listing.slug:
+ slug = job_listing.slug.localize('en')
+ assert slug
+ job_listing.slug = slug
+ if job_listing.title:
+ title = job_listing.title.localize('en')
+ assert title
+ job_listing.title = title
+ if job_listing.description:
+ description = job_listing.description.localize('en')
+ assert description
+ job_listing.description = description
+ job_listing.save()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('job_board', '0005_connect_job_listings_to_a_conf'),
+ ]
+
+ operations = [
+ migrations.RunPython(fix_data, migrations.RunPython.noop),
+ migrations.AlterField(
+ model_name='joblisting',
+ name='apply_url',
+ field=models.TextField(blank=True, verbose_name='Where you can apply'),
+ ),
+ migrations.AlterField(
+ model_name='joblisting',
+ name='description',
+ field=models.TextField(blank=True, verbose_name='description'),
+ ),
+ migrations.AlterField(
+ model_name='joblisting',
+ name='slug',
+ field=models.SlugField(blank=True, max_length=200, verbose_name='slug'),
+ ),
+ migrations.AlterField(
+ model_name='joblisting',
+ name='title',
+ field=models.TextField(max_length=200, verbose_name='title'),
+ ),
+ ]
diff --git a/backend/job_board/migrations/0007_auto_20250413_1608.py b/backend/job_board/migrations/0007_auto_20250413_1608.py
new file mode 100644
index 0000000000..36691df68e
--- /dev/null
+++ b/backend/job_board/migrations/0007_auto_20250413_1608.py
@@ -0,0 +1,30 @@
+# Generated by Django 5.1.4 on 2025-04-13 16:08
+
+from django.db import migrations
+
+def fix_quotes(apps, schema_editor):
+ JobListing = apps.get_model('job_board', 'JobListing')
+ for job_listing in JobListing.objects.all():
+ if job_listing.title:
+ title = job_listing.title.replace('"', "")
+ assert title
+ job_listing.title = title
+ if job_listing.description:
+ description = job_listing.description.replace('"', "")
+ assert description
+ job_listing.description = description
+ if job_listing.slug:
+ slug = job_listing.slug.replace('"', "")
+ assert slug
+ job_listing.slug = slug
+ job_listing.save()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('job_board', '0006_alter_joblisting_apply_url_and_more'),
+ ]
+
+ operations = [
+ migrations.RunPython(fix_quotes, migrations.RunPython.noop),
+ ]
diff --git a/backend/job_board/models.py b/backend/job_board/models.py
index a1a81d7265..5cb28c266b 100644
--- a/backend/job_board/models.py
+++ b/backend/job_board/models.py
@@ -1,35 +1,26 @@
from copy import copy
-from django.conf import settings
from django.db import models
-from django.db.models import Q
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from model_utils.models import TimeStampedModel
from ordered_model.models import OrderedModel, OrderedModelManager
-from i18n.fields import I18nCharField, I18nTextField
-
class JobListingManager(OrderedModelManager):
def by_slug(self, slug):
- filters = Q()
-
- for lang, __ in settings.LANGUAGES:
- filters |= Q(**{f"slug__{lang}": slug})
-
- return self.get_queryset().filter(filters)
+ return self.get_queryset().filter(slug=slug)
class JobListing(TimeStampedModel, OrderedModel):
- title = I18nCharField(_("title"), max_length=200)
- slug = I18nCharField(_("slug"), max_length=200, blank=True)
+ title = models.TextField(_("title"), max_length=200)
+ slug = models.SlugField(_("slug"), max_length=200, blank=True)
company = models.CharField(_("company"), max_length=100)
company_logo = models.ImageField(
_("company logo"), null=True, blank=True, upload_to="job-listings"
)
- description = I18nTextField(_("description"), blank=True)
- apply_url = models.TextField(_("URL where you can apply"), blank=True)
+ description = models.TextField(_("description"), blank=True)
+ apply_url = models.TextField(_("Where you can apply"), blank=True)
conference = models.ForeignKey(
"conferences.Conference",
on_delete=models.CASCADE,
diff --git a/frontend/src/components/job-board-layout/index.tsx b/frontend/src/components/job-board-layout/index.tsx
index 3df98a2bf3..da01c5ee31 100644
--- a/frontend/src/components/job-board-layout/index.tsx
+++ b/frontend/src/components/job-board-layout/index.tsx
@@ -7,13 +7,13 @@ import {
Page,
Section,
Spacer,
+ StyledHTMLText,
Text,
} from "@python-italia/pycon-styleguide";
import React from "react";
import { FormattedMessage } from "react-intl";
import { JobListingAccordion } from "~/components/job-listing-accordion";
-import { compile } from "~/helpers/markdown";
import type { AllJobListingsQueryResult } from "~/types";
import { Article } from "../article";
@@ -49,8 +49,12 @@ export const JobBoardLayout = ({
onMobileShowOnly === "jobListing" ? "desktop" : "mobile"
}
as="ul"
- fullScreenHeight
+ position="sticky"
+ style={{
+ top: 0,
+ }}
overflow="scroll"
+ fullScreenHeight
>
{jobListings.map((job) => (
@@ -62,8 +66,6 @@ export const JobBoardLayout = ({
showFrom={
onMobileShowOnly === "jobListings" ? "desktop" : "mobile"
}
- fullScreenHeight
- overflow="scroll"
>
{jobListing.title}
@@ -71,14 +73,24 @@ export const JobBoardLayout = ({
{jobListing.company}
- {compile(jobListing.description).tree}
+
+
+
{jobListing.applyUrl && (
-