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 && ( - )} + diff --git a/frontend/src/components/job-detail-page-handler/index.tsx b/frontend/src/components/job-detail-page-handler/index.tsx index a0a66a4a06..e29a2cd307 100644 --- a/frontend/src/components/job-detail-page-handler/index.tsx +++ b/frontend/src/components/job-detail-page-handler/index.tsx @@ -11,12 +11,10 @@ export const JobDetailPageHandler = () => { const { query: { id }, } = useRouter(); - const language = useCurrentLanguage(); const { data: { jobListings }, } = useAllJobListingsQuery({ variables: { - language, conference: process.env.conferenceCode, }, }); diff --git a/frontend/src/components/job-page-handler/index.tsx b/frontend/src/components/job-page-handler/index.tsx index 3ec1bcf595..7399debe61 100644 --- a/frontend/src/components/job-page-handler/index.tsx +++ b/frontend/src/components/job-page-handler/index.tsx @@ -6,12 +6,10 @@ import { useAllJobListingsQuery } from "~/types"; import { JobBoardLayout } from "../job-board-layout"; export const JobPageHandler = () => { - const language = useCurrentLanguage(); const { data: { jobListings }, } = useAllJobListingsQuery({ variables: { - language, conference: process.env.conferenceCode, }, }); diff --git a/frontend/src/components/job-page-handler/jobs-page.graphql b/frontend/src/components/job-page-handler/jobs-page.graphql index ce92c33fef..9216e7b8da 100644 --- a/frontend/src/components/job-page-handler/jobs-page.graphql +++ b/frontend/src/components/job-page-handler/jobs-page.graphql @@ -1,9 +1,9 @@ -query AllJobListings($language: String!, $conference: String!) { +query AllJobListings($conference: String!) { jobListings(conference: $conference) { id - slug(language: $language) - title(language: $language) - description(language: $language) + slug + title + description applyUrl company companyLogo diff --git a/frontend/src/pages/jobs/[id].tsx b/frontend/src/pages/jobs/[id].tsx index 7111996040..2edec61e68 100644 --- a/frontend/src/pages/jobs/[id].tsx +++ b/frontend/src/pages/jobs/[id].tsx @@ -10,7 +10,6 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => { await Promise.all([ prefetchSharedQueries(client, locale), queryAllJobListings(client, { - language: locale, conference: process.env.conferenceCode, }), ]); @@ -24,26 +23,19 @@ export const getStaticPaths: GetStaticPaths = async () => { const client = getApolloClient(); const { - data: { jobListings: italianJobListings }, + data: { jobListings }, } = await queryAllJobListings(client, { - language: "it", - conference: process.env.conferenceCode, - }); - const { - data: { jobListings: englishJobListings }, - } = await queryAllJobListings(client, { - language: "en", conference: process.env.conferenceCode, }); const paths = [ - ...italianJobListings.map((page) => ({ + ...jobListings.map((page) => ({ params: { id: page.id, }, locale: "it", })), - ...englishJobListings.map((page) => ({ + ...jobListings.map((page) => ({ params: { id: page.id, }, diff --git a/frontend/src/pages/jobs/index.tsx b/frontend/src/pages/jobs/index.tsx index 9a88be728f..688130261d 100644 --- a/frontend/src/pages/jobs/index.tsx +++ b/frontend/src/pages/jobs/index.tsx @@ -10,7 +10,6 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => { await Promise.all([ prefetchSharedQueries(client, locale), queryAllJobListings(client, { - language: locale, conference: process.env.conferenceCode, }), ]); diff --git a/infrastructure/applications/database/db.tf b/infrastructure/applications/database/db.tf index 5b6141eb5f..3a7df77f88 100644 --- a/infrastructure/applications/database/db.tf +++ b/infrastructure/applications/database/db.tf @@ -9,7 +9,7 @@ resource "aws_db_instance" "database" { engine = "postgres" identifier = "pythonit-${terraform.workspace}" allow_major_version_upgrade = true - engine_version = "14.12" + engine_version = "14.13" instance_class = "db.t4g.micro" db_name = local.is_prod ? "${local.normalized_workspace}backend" : "pycon" username = "root"