Skip to content

Commit bdeedd1

Browse files
Ahtesham QuraishAhtesham Quraish
authored andcommitted
address the feedback
1 parent 3f1359b commit bdeedd1

19 files changed

Lines changed: 184 additions & 96 deletions

File tree

frontends/api/src/generated/v1/api.ts

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

news_events/etl/articles_news.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
from news_events.constants import FeedType
66
from news_events.etl import loaders
7-
from website_content.constants import CONTENT_TYPE_NEWS
7+
from website_content.constants import WebsiteContentType
88
from website_content.models import WebsiteContent
99

1010
log = logging.getLogger(__name__)
1111

1212

13-
def extract_single_article(article: WebsiteContent) -> dict:
13+
def extract_single_website_content(article: WebsiteContent) -> dict:
1414
"""
1515
Extract a single published news content item from the database.
1616
Returns a dict in the same format as extract().
@@ -36,16 +36,16 @@ def transform_single_article(article_data: dict) -> dict:
3636
return items[0] if items else None
3737

3838

39-
def sync_single_article_to_news(article: WebsiteContent):
39+
def sync_single_website_content_news_to_news(article: WebsiteContent):
4040
"""
4141
Sync a single published news content item to the news feed.
4242
Only syncs content items with content_type='news'.
4343
"""
4444
if not article.is_published:
4545
return
46-
if article.content_type != CONTENT_TYPE_NEWS:
46+
if article.content_type != WebsiteContentType.news.name:
4747
return
48-
article_data = extract_single_article(article)
48+
article_data = extract_single_website_content(article)
4949
item_data = transform_single_article(article_data)
5050
if not item_data:
5151
return
@@ -72,7 +72,7 @@ def extract() -> list[dict]:
7272
"""
7373
# Get only published news-type content items
7474
articles = WebsiteContent.objects.filter(
75-
is_published=True, content_type=CONTENT_TYPE_NEWS
75+
is_published=True, content_type=WebsiteContentType.news.name
7676
).select_related("user")
7777

7878
return [

news_events/etl/articles_news_test.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ class MockArticle:
564564
publish_date = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
565565
user = User()
566566

567-
result = articles_news.extract_single_article(MockArticle())
567+
result = articles_news.extract_single_website_content(MockArticle())
568568

569569
assert result["id"] == 1
570570
assert result["title"] == "Test Article"
@@ -640,7 +640,7 @@ class MockArticle:
640640
user = User()
641641

642642
# Sync the article
643-
articles_news.sync_single_article_to_news(MockArticle())
643+
articles_news.sync_single_website_content_news_to_news(MockArticle())
644644

645645
# Verify FeedSource was created/retrieved
646646
mock_get_or_create.assert_called_once()
@@ -661,7 +661,20 @@ class MockArticle:
661661
is_published = False
662662

663663
# Sync the article
664-
articles_news.sync_single_article_to_news(MockArticle())
664+
articles_news.sync_single_website_content_news_to_news(MockArticle())
665665

666666
# Verify load_feed_item was NOT called
667667
mock_load_feed_item.assert_not_called()
668+
669+
670+
def test_sync_single_article_wrong_content_type(mocker):
671+
"""Test that articles with a content_type other than 'news' are not synced"""
672+
mock_load_feed_item = mocker.patch("news_events.etl.loaders.load_feed_item")
673+
674+
class MockArticle:
675+
is_published = True
676+
content_type = "article" # not WebsiteContentType.news
677+
678+
articles_news.sync_single_website_content_news_to_news(MockArticle())
679+
680+
mock_load_feed_item.assert_not_called()

news_events/plugins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ def website_content_published(self, content):
2121
Args:
2222
content (WebsiteContent): The content item that was published or updated
2323
"""
24-
from website_content.constants import CONTENT_TYPE_NEWS
24+
from website_content.constants import WebsiteContentType
2525

26-
if content.content_type != CONTENT_TYPE_NEWS:
26+
if content.content_type != WebsiteContentType.news.name:
2727
log.info(
2828
"WebsiteContentNewsPlugin: Skipping non-news content: id=%s, type=%s",
2929
content.id,

news_events/tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,14 @@ def sync_website_content_to_news(self, content_id: int):
7676
"""
7777
import logging
7878

79-
from news_events.etl.articles_news import sync_single_article_to_news
79+
from news_events.etl.articles_news import sync_single_website_content_news_to_news
8080
from website_content.models import WebsiteContent
8181

8282
logger = logging.getLogger(__name__)
8383

8484
try:
8585
content = WebsiteContent.objects.get(id=content_id, is_published=True)
86-
sync_single_article_to_news(content)
86+
sync_single_website_content_news_to_news(content)
8787
clear_views_cache()
8888
logger.info(
8989
"Successfully synced content %s to news feed",

news_events/tasks_test.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def test_sync_article_to_news_success(mocker, user):
6666
)
6767

6868
mock_sync = mocker.patch(
69-
"news_events.etl.articles_news.sync_single_article_to_news", autospec=True
69+
"news_events.etl.articles_news.sync_single_website_content_news_to_news",
70+
autospec=True,
7071
)
7172
mock_clear_cache = mocker.patch(
7273
"news_events.tasks.clear_views_cache", autospec=True
@@ -82,7 +83,8 @@ def test_sync_article_to_news_success(mocker, user):
8283
def test_sync_article_to_news_article_not_found(mocker, caplog):
8384
"""Task should log warning if content item doesn't exist"""
8485
mock_sync = mocker.patch(
85-
"news_events.etl.articles_news.sync_single_article_to_news", autospec=True
86+
"news_events.etl.articles_news.sync_single_website_content_news_to_news",
87+
autospec=True,
8688
)
8789
mock_clear_cache = mocker.patch(
8890
"news_events.tasks.clear_views_cache", autospec=True
@@ -110,7 +112,8 @@ def test_sync_article_to_news_unpublished_article(mocker, user, caplog):
110112
)
111113

112114
mock_sync = mocker.patch(
113-
"news_events.etl.articles_news.sync_single_article_to_news", autospec=True
115+
"news_events.etl.articles_news.sync_single_website_content_news_to_news",
116+
autospec=True,
114117
)
115118
mock_clear_cache = mocker.patch(
116119
"news_events.tasks.clear_views_cache", autospec=True
@@ -138,7 +141,7 @@ def test_sync_article_to_news_sync_failure(mocker, user):
138141
)
139142

140143
mock_sync = mocker.patch(
141-
"news_events.etl.articles_news.sync_single_article_to_news",
144+
"news_events.etl.articles_news.sync_single_website_content_news_to_news",
142145
autospec=True,
143146
side_effect=Exception("Sync failed"),
144147
)

openapi/specs/v1.yaml

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14361,8 +14361,7 @@ components:
1436114361
author_name:
1436214362
type: string
1436314363
default: ''
14364-
content:
14365-
default: {}
14364+
content: {}
1436614365
content_type:
1436714366
allOf:
1436814367
- $ref: '#/components/schemas/WebsiteContentContentTypeEnum'
@@ -17437,8 +17436,7 @@ components:
1743717436
author_name:
1743817437
type: string
1743917438
default: ''
17440-
content:
17441-
default: {}
17439+
content: {}
1744217440
content_type:
1744317441
allOf:
1744417442
- $ref: '#/components/schemas/WebsiteContentContentTypeEnum'
@@ -17478,11 +17476,11 @@ components:
1747817476
- article
1747917477
type: string
1748017478
description: |-
17481-
* `news` - news
17482-
* `article` - article
17479+
* `news` - News
17480+
* `article` - Article
1748317481
x-enum-descriptions:
17484-
- news
17485-
- article
17482+
- News
17483+
- Article
1748617484
WebsiteContentImageUploadRequest:
1748717485
type: object
1748817486
properties:
@@ -17502,8 +17500,7 @@ components:
1750217500
author_name:
1750317501
type: string
1750417502
default: ''
17505-
content:
17506-
default: {}
17503+
content: {}
1750717504
content_type:
1750817505
allOf:
1750917506
- $ref: '#/components/schemas/WebsiteContentContentTypeEnum'

profiles/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ def profile_image_upload_uri(instance, filename):
118118
def article_image_upload_uri(_, filename):
119119
"""
120120
upload_to handler for ArticleImageUpload.image_file
121+
122+
Deprecated: use website_content.utils.website_content_image_upload_uri instead.
123+
Kept here so existing migrations that reference this dotted path remain replayable.
121124
"""
122125
return generate_filepath(filename, "", "", "article")
123126

website_content/api.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44

5+
from website_content.constants import WebsiteContentType
56
from website_content.hooks import get_plugin_manager
67
from website_content.tasks import (
78
PURGE_TIMEOUT_SECONDS,
@@ -11,6 +12,11 @@
1112

1213
log = logging.getLogger(__name__)
1314

15+
_CONTENT_TYPE_LISTING_URL = {
16+
WebsiteContentType.news.name: "/news",
17+
WebsiteContentType.article.name: "/articles",
18+
}
19+
1420

1521
def purge_content_on_save(content):
1622
"""
@@ -44,7 +50,9 @@ def purge_content_on_save(content):
4450
fastly_purge_relative_url.delay(content_url)
4551
log.exception("Content purge request failed, enqueued for retry.")
4652

47-
fastly_purge_website_content_list.delay()
53+
fastly_purge_website_content_list.delay(
54+
_CONTENT_TYPE_LISTING_URL.get(content.content_type, "/news")
55+
)
4856
else:
4957
log.debug(
5058
"WebsiteContent %s is not published or has no slug, skipping CDN purge.",

website_content/api_test.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,81 @@
22

33
import pytest
44

5+
from website_content.api import purge_content_on_save
6+
from website_content.constants import WebsiteContentType
7+
from website_content.factories import WebsiteContentFactory
8+
9+
10+
@pytest.mark.django_db
11+
@pytest.mark.parametrize(
12+
("slug", "is_published", "expect_purge"),
13+
[
14+
("test-article", True, True),
15+
("test-article", False, False),
16+
("", True, False),
17+
("", False, False),
18+
],
19+
)
20+
def test_purge_content_on_save(mocker, slug, is_published, expect_purge):
21+
"""
22+
CDN purge should only fire when the content is published AND has a slug.
23+
Asserts on call_fastly_purge_api (the lowest plumbing before the HTTP call)
24+
so the test is independent of FASTLY_API_KEY being set in the test env.
25+
@single_task uses a Redis lock so fastly_purge_website_content_list.delay
26+
is mocked to avoid that dependency in unit tests.
27+
"""
28+
mock_purge_api = mocker.patch("website_content.tasks.call_fastly_purge_api")
29+
mock_purge_api.return_value = {"status": "ok", "id": "abc"}
30+
mocker.patch("website_content.tasks.fastly_purge_website_content_list.delay")
31+
32+
content = WebsiteContentFactory.build(
33+
is_published=is_published,
34+
slug=slug or None,
35+
content_type=WebsiteContentType.news.name,
36+
)
37+
38+
purge_content_on_save(content)
39+
40+
if expect_purge:
41+
assert mock_purge_api.called
42+
else:
43+
mock_purge_api.assert_not_called()
44+
45+
46+
@pytest.mark.django_db
47+
@pytest.mark.parametrize(
48+
("content_type", "expected_listing"),
49+
[
50+
(WebsiteContentType.news.name, "/news"),
51+
(WebsiteContentType.article.name, "/articles"),
52+
],
53+
)
54+
def test_purge_content_on_save_listing_url(mocker, content_type, expected_listing):
55+
"""
56+
The listing-page purge task should be enqueued with the URL matching
57+
the content's content_type.
58+
"""
59+
mocker.patch("website_content.tasks.call_fastly_purge_api").return_value = {
60+
"status": "ok",
61+
"id": "abc",
62+
}
63+
mock_purge_list = mocker.patch(
64+
"website_content.tasks.fastly_purge_website_content_list.delay"
65+
)
66+
67+
content = WebsiteContentFactory.build(
68+
is_published=True,
69+
slug="some-slug",
70+
content_type=content_type,
71+
)
72+
mocker.patch.object(
73+
type(content), "get_url", return_value=f"/{content_type}/some-slug"
74+
)
75+
76+
purge_content_on_save(content)
77+
78+
mock_purge_list.assert_called_once_with(expected_listing)
79+
580

681
@pytest.mark.django_db
782
def test_content_published_actions_triggers_hook(mocker, user):

0 commit comments

Comments
 (0)