Skip to content
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
904fde2
feat: add description box to create post section
javiercoronadonarvaez May 27, 2026
d19ae6c
fix: add placeholder to Content section and grip to expand text area
javiercoronadonarvaez May 27, 2026
0872d78
feat: add Auto-generate description functionality
javiercoronadonarvaez May 27, 2026
b5b8af0
feat: add Saving and Saved state and icons for Description section
javiercoronadonarvaez May 27, 2026
49ac1a1
feat: store Content and Description in browser localstorage
javiercoronadonarvaez May 27, 2026
0218751
fix: revert Content section to wysiwyg component
javiercoronadonarvaez May 28, 2026
9c5880b
fix: apply black and exclude generated esbuild bundles from pre-commit
javiercoronadonarvaez May 28, 2026
a472baa
fix: format tasks.py with black 24.10.0 to match pre-commit
javiercoronadonarvaez May 28, 2026
ba7c8c7
feat: add placeholder to description box while summary is being gener…
javiercoronadonarvaez May 28, 2026
428ea95
fix: maintain responsiveness with design tokens
javiercoronadonarvaez May 28, 2026
4be282e
fix: reconfigure grip location in Content box to match that of Descri…
javiercoronadonarvaez May 28, 2026
d6d1f5d
chore: delete development plan file
javiercoronadonarvaez May 29, 2026
ca2d0d1
feat: enable auto generated description display on post entry page af…
javiercoronadonarvaez May 29, 2026
7921c7b
feat: clear localstorage after successful post submit
javiercoronadonarvaez May 29, 2026
59b5547
fix: update test_forms.py to persist user-provided description to Ent…
javiercoronadonarvaez May 29, 2026
33d080a
refactor: bake viewBox into saving/saved icons so callers can't pass …
javiercoronadonarvaez Jun 1, 2026
0ee4ca6
fix: provide the default as part of the get call for both content and…
javiercoronadonarvaez Jun 1, 2026
7d44244
refactor: read the OpenRouter model name from env via settings
javiercoronadonarvaez Jun 3, 2026
3e25023
refactor: extract generate_summary helper so view gets a timeout and …
javiercoronadonarvaez Jun 3, 2026
51d552c
fix: apply black 24.10.0 formatting
javiercoronadonarvaez Jun 3, 2026
ec3958e
feat: add Auto-Generate description button for Links Post type
javiercoronadonarvaez Jun 2, 2026
aa05dcc
feat: add saving/saved status for written description in Link post type
javiercoronadonarvaez Jun 2, 2026
02f22de
feat: require https://cppalliance.org/ and .html path Link post section
javiercoronadonarvaez Jun 3, 2026
ef53389
feat: add title and text extractor from Link's html input
javiercoronadonarvaez Jun 3, 2026
2c99bf3
feat: link auto generate button to Link's html extracted text as input
javiercoronadonarvaez Jun 3, 2026
74b877d
fix: apply black 24.10.0 formatting
javiercoronadonarvaez Jun 3, 2026
d024ef8
feat: surface Link description if auto or manually generated to avoid…
javiercoronadonarvaez Jun 3, 2026
3b38304
feat: add character counter to Link's Description section
javiercoronadonarvaez Jun 3, 2026
671b8a7
fix: display link instructions for correct input shape when the provi…
javiercoronadonarvaez Jun 4, 2026
b8a63a4
feat: add truncation for auto generated summary
javiercoronadonarvaez Jun 8, 2026
ea165c4
feat: persist title for all Posts to local storage
javiercoronadonarvaez Jun 8, 2026
1bbccec
fix: apply black 26.1.0 formatting
javiercoronadonarvaez Jun 8, 2026
a039e95
merge: bring 2428 (blog/news AI description + black 26.1.0) into 2451
javiercoronadonarvaez Jun 8, 2026
c2007f6
feat: implement general parsing strategy for Links with trafilatura
javiercoronadonarvaez Jun 8, 2026
a3ebdd1
fix: guard server-side link fetches against SSRF
javiercoronadonarvaez Jun 8, 2026
3dd05d6
fix: replicate description section across all kinds of Posts
javiercoronadonarvaez Jun 9, 2026
cce6fa3
feat: persist URL for both Video and Link type via localstorage
javiercoronadonarvaez Jun 9, 2026
2aebc6b
fix: bump lxml>=6.1.0 and urllib3>=2.7.0 to patch advisories
javiercoronadonarvaez Jun 9, 2026
9729724
feat: enable safe_get to cap the response body at MAX_FETCH_BYTES
javiercoronadonarvaez Jun 9, 2026
c3979af
fix: redact fetched content from logs and skip empty summary saves
javiercoronadonarvaez Jun 9, 2026
307a692
fix: address PR review on drafts persistence and SSRF docstring update
javiercoronadonarvaez Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
default_language_version:
python: python3.13

exclude: .*migrations\/.*|static\/img\/.*|static\/animations\/.*|static\/js\/boost-gecko\/.*|kube\/boost\/templates\/.*\.yaml
# Generated esbuild outputs (see `build:wysiwyg` / `build:fuse` in package.json)
# are excluded because trailing-whitespace / EOL hooks would corrupt minified
# string and regex content inside them.
exclude: .*migrations\/.*|static\/img\/.*|static\/animations\/.*|static\/js\/boost-gecko\/.*|kube\/boost\/templates\/.*\.yaml|static\/js\/v3\/wysiwyg-editor\.js|static\/js\/v3\/fuse\.min\.js

repos:
- repo: https://github.com/adamchainz/django-upgrade
rev: "1.27.0"
hooks:
- id: django-upgrade
args: [--target-version, "5.2"] # Replace with Django version
args: [--target-version, "5.2"] # Replace with Django version
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
Expand All @@ -20,7 +23,7 @@ repos:
- id: trailing-whitespace
exclude: ^core/tests/content/boost_release[a-zA-Z_]+.html
- repo: https://github.com/ambv/black
rev: 24.10.0
rev: 26.1.0
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
Expand Down
1 change: 0 additions & 1 deletion ak/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from testimonials.models import Testimonial
from core.mock_data import SharedResources


logger = structlog.get_logger()


Expand Down
2 changes: 2 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,8 @@
BOOST_BRANCHES = ["master", "develop"]
OPENROUTER_URL = "https://openrouter.ai/api/v1"
OPENROUTER_API_KEY = env("OPENROUTER_API_KEY")
SUMMARIZATION_MODEL = env("SUMMARIZATION_MODEL", default="gpt-oss-120b")
WHATS_NEW_MODEL = env("WHATS_NEW_MODEL", default="gpt-oss-120b")

ALGOLIA = {
"app_id": env("ALGOLIA_APP_ID", None),
Expand Down
16 changes: 15 additions & 1 deletion config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@
CommitEmailResendView,
)
from news.feeds import AtomNewsFeed, RSSNewsFeed
from news.views import V3AllTypesCreateView
from news.views import (
V3AllTypesCreateView,
generate_description,
generate_link_description,
)
from users.views import (
CurrentUserAPIView,
CurrentUserProfileView,
Expand Down Expand Up @@ -271,6 +275,16 @@
),
path("news/", include("news.urls")),
path("v3/news/add/", V3AllTypesCreateView.as_view(), name="v3-news-create"),
path(
"v3/news/generate-description/",
generate_description,
name="v3-news-generate-description",
),
path(
"v3/news/generate-link-description/",
generate_link_description,
name="v3-news-generate-link-description",
),
path(
"people/detail/",
TemplateView.as_view(template_name="boost/people_detail.html"),
Expand Down
1 change: 0 additions & 1 deletion core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from versions.converters import BoostVersionSlugConverter
from versions.models import Version


_BOOST_VERSION_SLUG_ROUTE_TOKEN = (
f"<{BoostVersionSlugConverter.URL_TYPE_NAME}:version_slug>"
)
Expand Down
1 change: 0 additions & 1 deletion core/tests/test_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from ..models import RenderedContent


TEST_CACHES = {
"static_content": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
Expand Down
1 change: 0 additions & 1 deletion core/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
clear_rendered_content_cache_by_content_type,
)


TEST_CACHES = {
"static_content": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
Expand Down
6 changes: 2 additions & 4 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1954,8 +1954,7 @@ def get_context_data(self, **kwargs):
]
context["markdown_data"] = {
"title": "Markdown Block",
"markdown": dedent(
"""
"markdown": dedent("""

######Insert anything Required

Expand All @@ -1965,8 +1964,7 @@ def get_context_data(self, **kwargs):
* list

Or **bold** and *italics* and whatever it needs to be formatted or [use links](https://www.example.com)!
"""
),
"""),
"button_url": "#",
"button_label": "Optional CTA Button",
"button_style": "primary",
Expand Down
38 changes: 38 additions & 0 deletions frontend/wysiwyg-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -891,11 +891,49 @@ export const initWysiwyg = (textareaId) => {
form.addEventListener("submit", syncTextarea, true);
}

// ── Bridge to the host page (e.g. the create-post Alpine form) ──────────
// Emit content + plain-text char count on every change so the page can drive
// a char counter, a Saving/Saved indicator, and localStorage persistence.
// `programmatic: true` flags updates the user didn't make (initial load /
// restore) so the page can skip the "Saving" animation and not re-persist.
const currentValue = () =>
state.mode === "markdown"
? state.markdownText
: turndown.turndown(editor.getHTML());
const dispatchState = (programmatic) => {
editorEl.dispatchEvent(
new CustomEvent("wysiwyg-update", {
detail: {
id: textareaId,
characters: editor.state.doc.textContent.length,
value: currentValue(),
programmatic: !!programmatic,
},
bubbles: true,
}),
);
};
editor.on("update", () => dispatchState(false));

// Let the host push a saved draft back into the editor (restore).
const onSetContent = (e) => {
if (!e.detail || e.detail.id !== textareaId) return;
const md = e.detail.value || "";
editor.commands.setContent(md ? parseMarkdownSafe(md) : "");
dispatchState(true);
};
window.addEventListener("wysiwyg-set-content", onSetContent);

// Initial state (deferred a frame so the host's listener is attached).
dispatchState(true);
requestAnimationFrame(() => dispatchState(true));

editorInstances.set(textareaId, {
editor,
cleanup: () => {
document.removeEventListener("click", handleDocClick);
if (form) form.removeEventListener("submit", syncTextarea, true);
window.removeEventListener("wysiwyg-set-content", onSetContent);
},
});
return editor;
Expand Down
1 change: 0 additions & 1 deletion gunicorn.conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import os
from psycogreen.gevent import patch_psycopg # use this if you use gevent workers


BASE_DIR = os.environ["H"] if os.environ.get("H", None) else "/code"

accesslog = "-"
Expand Down
3 changes: 1 addition & 2 deletions libraries/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
)
from .utils import generate_release_report_filename


logger = structlog.get_logger()


Expand Down Expand Up @@ -277,7 +276,7 @@ def generate_report(self):
# NOTE TO FUTURE DEVS: remember to account for the fact that a report
# configuration may not match with a real version in frequent cases where
# reports are generated before the release version has been created.
(report_before_release, prior_version, version) = determine_versions(
report_before_release, prior_version, version = determine_versions(
report_configuration.version
)

Expand Down
14 changes: 6 additions & 8 deletions libraries/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def get_stats(self, base_uri: str = None):
# NOTE TO FUTURE DEVS: remember to account for the fact that a report
# configuration may not match with a real version in frequent cases where
# reports are generated before the release version has been created.
(report_before_release, prior_version, version) = determine_versions(
report_before_release, prior_version, version = determine_versions(
report_configuration.version
)

Expand Down Expand Up @@ -368,21 +368,19 @@ def core_batchable(x):
git_graph_data = get_git_graph_data(prior_version, version)
download = get_download_links(version, base_uri)
### completed task handling ###
(mailinglist_contributor_release_count, mailinglist_contributor_new_count) = (
mailinglist_contributor_release_count, mailinglist_contributor_new_count = (
mailing_list_contributors_task.get()
)
(mailinglist_post_stats, total_mailinglist_count) = (
mailing_list_stats_task.get()
)
(commit_contributors_release_count, commit_contributors_new_count) = (
mailinglist_post_stats, total_mailinglist_count = mailing_list_stats_task.get()
commit_contributors_release_count, commit_contributors_new_count = (
commit_contributors_task.get()
)
(
mailinglist_words,
mailinglist_wordcloud_base64,
mailinglist_wordcloud_top_words,
) = mailinglist_wordcloud_task.get()
(search_wordcloud_base64, search_wordcloud_top_words, search_stats) = (
search_wordcloud_base64, search_wordcloud_top_words, search_stats = (
Comment on lines +371 to +383

@coderabbitai coderabbitai Bot Jun 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unused unpacked value in task result handling.

mailinglist_words is unpacked but never used. Rename it to _mailinglist_words (or _) to make intent explicit and satisfy lint rules.

🧰 Tools
🪛 Ruff (0.15.15)

[warning] 379-379: Unpacked variable mailinglist_words is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/forms.py` around lines 371 - 383, The tuple unpacking from
mailinglist_wordcloud_task.get() includes an unused first element; change the
variable name mailinglist_words to _mailinglist_words (or just _) in the
unpacking expression inside the function where mailinglist_wordcloud_task.get()
is called so the unused value is explicit and linter warnings are resolved
(leave mailinglist_wordcloud_base64 and mailinglist_wordcloud_top_words
unchanged).

Source: Linters/SAST tools

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of scope for this PR. They're pre-existing lint, unrelated to anything changed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, thanks for the context. Would you like me to open a follow-up GitHub issue to track the cleanup of the unused mailinglist_words variable so it doesn't get lost?

(ᴖ̥̥̥̥ ‸ ᴖ̥̥̥̥)

search_wordcloud_task.get()
)
global_contributors_new_count = new_contributors_count_task.get()
Expand Down Expand Up @@ -441,7 +439,7 @@ def generate_context(
# NOTE TO FUTURE DEVS: remember to account for the fact that a report
# configuration may not match with a real version in frequent cases where
# reports are generated before the release version has been created.
(report_before_release, prior_version, version) = determine_versions(
report_before_release, prior_version, version = determine_versions(
report_configuration.version
)
Comment on lines +442 to 444

@coderabbitai coderabbitai Bot Jun 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unused value from determine_versions unpacking.

report_before_release is assigned but unused in generate_context. Replace with _report_before_release (or _) to avoid dead bindings and keep lint clean.

🧰 Tools
🪛 Ruff (0.15.15)

[warning] 442-442: Unpacked variable report_before_release is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/forms.py` around lines 442 - 444, The call to determine_versions in
generate_context currently unpacks three values into report_before_release,
prior_version, version but report_before_release is never used; change the
unpack to use a throwaway name (e.g. _report_before_release or _) for the first
element while keeping prior_version and version so lint stops flagging an unused
binding—locate the determine_versions call inside generate_context and replace
report_before_release with the chosen underscore variable.

Source: Linters/SAST tools

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of scope for this PR. They're pre-existing lint, unrelated to anything changed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, @javiercoronadonarvaez — the formatting change here is purely cosmetic (parens removal) and the RUF059 warning on report_before_release is indeed pre-existing. I'll dismiss this comment.

Would you like me to open a follow-up GitHub issue to track fixing the unused report_before_release binding in generate_context (and the similar one in get_stats) so it doesn't get lost?

(´• ω •`) 🐇


Expand Down
6 changes: 2 additions & 4 deletions libraries/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,12 @@ def github_action_boostdep_output_artifact():
A shortened example of the content in the artifact output by the
dependency_report.yaml action
"""
return dedent(
"""\
return dedent("""\
Dependencies for version boost-1.33.0
Dependencies for version boost-1.34.0
Dependencies for version boost-1.35.0
algorithm -> concept_check config detail logic numeric~conversion
Dependencies for version boost-1.85.0
algorithm -> array assert bind concept_check config core
numeric~conversion -> array
callable_traits ->"""
)
callable_traits ->""")
1 change: 0 additions & 1 deletion libraries/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from django.contrib.auth import get_user_model
from django.core.management import call_command


User = get_user_model()


Expand Down
6 changes: 2 additions & 4 deletions libraries/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,12 @@ def test_get_and_store_library_version_documentation_urls_for_version(
version = library_version.version
library = library_version.library
library_name = library.name.lower()
mock_s3_response = {
"content": f"""
mock_s3_response = {"content": f"""
<h2>Libraries Listed <a name="Alphabetically">Alphabetically</a></h2>
<ul>
<li><a href="{library_name}/index.html">{library_name}</a></li>
</ul>
"""
}
"""}

# Mock the get_content_from_s3 function to return the mock S3 response
mock_s3_client.get_object.return_value = mock_s3_response
Expand Down
6 changes: 2 additions & 4 deletions mailing_list/management/commands/sync_mailinglist_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,13 @@ def bulk_create(rows):
columns = ["email", "name"]
# Uses a named cursor to use a serverside postgres cursor
with conn.cursor(name="commitauthor_sync") as cursor:
cursor.execute(
"""
cursor.execute("""
SELECT
LOWER(sender_id) AS email
, (ARRAY_AGG(distinct(sender_name)))[1] as name
FROM hyperkitty_email
GROUP BY LOWER(sender_id);
"""
)
""")
rows = []
for i, data in enumerate(cursor):
row = {x: data[j] for j, x in enumerate(columns)}
Expand Down
1 change: 0 additions & 1 deletion mailing_list/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from config.celery import app


logger = structlog.getLogger(__name__)


Expand Down
2 changes: 1 addition & 1 deletion marketing/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_qrc_falls_back_to_remote_addr_when_no_xff(tp):
tp.response_302(res)
assert res["Location"] == "/library/latest/algorithm/"

(_, kwargs) = post_mock.call_args
_, kwargs = post_mock.call_args
headers = kwargs["headers"]
assert headers["X-Forwarded-For"] == "127.0.0.1" # Django test client default

Expand Down
1 change: 0 additions & 1 deletion news/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.contrib.auth import get_user_model
from django.db.models import Q


User = get_user_model()


Expand Down
3 changes: 3 additions & 0 deletions news/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
NEWS_APPROVAL_SALT = "news-approval"
MAGIC_LINK_EXPIRATION = 3600 * 24 # 24h
CONTENT_SUMMARIZATION_THRESHOLD = 1000 # characters
# Target length for the AI-generated Description. Kept under the 1000-char field
# cap so the model has some leeway and the result fits without truncation.
DESCRIPTION_SUMMARY_MAX_LENGTH = 900 # characters
6 changes: 3 additions & 3 deletions news/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ def save(self, *args, commit=True, **kwargs):
class BlogPostForm(EntryForm):
class Meta:
model = BlogPost
fields = ["title", "publish_at", "content", "image"]
fields = ["title", "publish_at", "content", "summary", "image"]


class LinkForm(EntryForm):
class Meta:
model = Link
fields = ["title", "publish_at", "external_url", "image"]
fields = ["title", "publish_at", "external_url", "summary", "image"]

# Holding on this as it's a new feature Issue #437
# def save(self, *args, commit=True, **kwargs):
Expand All @@ -43,7 +43,7 @@ class Meta:
class NewsForm(EntryForm):
class Meta:
model = News
fields = ["title", "publish_at", "content", "image"]
fields = ["title", "publish_at", "content", "summary", "image"]


class PollForm(EntryForm):
Expand Down
Loading
Loading