Skip to content

Commit f27b9c5

Browse files
Merge branch 'feature-add-recommendations-to-article' into master
2 parents d4cf912 + a5f57d2 commit f27b9c5

File tree

7 files changed

+105
-3
lines changed

7 files changed

+105
-3
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 2.2.14 on 2020-08-04 18:28
2+
3+
from django.db import migrations
4+
import wagtail.core.blocks
5+
import wagtail.core.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('blog', '0012_auto_20200625_1631'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='blogarticlepage',
17+
name='recommended_articles',
18+
field=wagtail.core.fields.StreamField([('page', wagtail.core.blocks.PageChooserBlock(can_choose_root=False, page_type=['blog.BlogArticlePage']))], blank=True, null=True),
19+
),
20+
]

blog/models.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from wagtail.admin.edit_handlers import StreamFieldPanel
1313
from wagtail.contrib.table_block.blocks import TableBlock
1414
from wagtail.core import blocks
15+
from wagtail.core.blocks import PageChooserBlock
1516
from wagtail.core.fields import StreamField
1617
from wagtail.core.models import Page
1718
from wagtail.core.query import PageQuerySet
@@ -88,7 +89,7 @@ class BlogArticlePage(MixinSeoFields, Page, MixinPageMethods, GoogleAdsMixin):
8889
("paragraph", blocks.RichTextBlock()),
8990
("table", TableBlock()),
9091
("image", ImageChooserBlock()),
91-
]
92+
],
9293
)
9394

9495
search_fields = Page.search_fields + [index.SearchField("intro"), index.SearchField("body")]
@@ -97,6 +98,10 @@ class BlogArticlePage(MixinSeoFields, Page, MixinPageMethods, GoogleAdsMixin):
9798

9899
read_time = models.PositiveIntegerField()
99100

101+
recommended_articles = StreamField(
102+
[("page", PageChooserBlock(can_choose_root=False, page_type="blog.BlogArticlePage"))], null=True, blank=True,
103+
)
104+
100105
views = models.PositiveIntegerField(default=0)
101106

102107
cover_photo = models.ForeignKey(
@@ -114,6 +119,7 @@ class BlogArticlePage(MixinSeoFields, Page, MixinPageMethods, GoogleAdsMixin):
114119
FieldPanel("intro"),
115120
FieldPanel("author"),
116121
FieldPanel("read_time"),
122+
StreamFieldPanel("recommended_articles"),
117123
FieldPanel("views"),
118124
FieldPanel("is_main_article"),
119125
ImageChooserPanel("cover_photo"),
@@ -156,6 +162,7 @@ def save(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=signature-
156162
def clean(self) -> None:
157163
super().clean()
158164
self._validate_title_length()
165+
self._validate_recommended_articles_uniqueness()
159166

160167
def _validate_parent_page(self) -> None:
161168
if not isinstance(self.get_parent().specific, BlogIndexPage):
@@ -164,3 +171,11 @@ def _validate_parent_page(self) -> None:
164171
def _validate_title_length(self) -> None:
165172
if self.title is not None and len(self.title) > MAX_BLOG_ARTICLE_TITLE_LENGTH:
166173
raise ValidationError({"title": f"Title must be less than {MAX_BLOG_ARTICLE_TITLE_LENGTH} characters."})
174+
175+
def _validate_recommended_articles_uniqueness(self) -> None:
176+
article_pages_set = set()
177+
for stream_child in self.recommended_articles: # pylint: disable=not-an-iterable
178+
if stream_child.value in article_pages_set:
179+
raise ValidationError(message=f"'{stream_child.value}' is listed more than once!")
180+
else:
181+
article_pages_set.add(stream_child.value)

blog/static/blog_post/blog_post.sass

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,6 @@ $width-to-change-details: 500px
123123
@media only screen and (min-width: $post-width-in-px)
124124
width: $post-width-in-px
125125
margin: 25px auto
126+
127+
.recommendations
128+
margin-bottom: 25px

blog/templates/blog/post.haml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@
3030
.article-content
3131
{{ page.specific.body }}
3232

33+
{% if page.specific.recommended_articles %}
34+
.recommendations
35+
%h4 See also:
36+
%ul
37+
{% for article in page.specific.recommended_articles %}
38+
%li {{ article }}
39+
{% endfor %}
40+
{% endif %}
41+
3342
.comment-section{:id => "disqus_thread"}
3443
%script{:type => "text/javascript"}
3544
var blog_page_url = {{ request.build_absolute_uri }}

blog/tests/test_blog_article_page.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.test import TestCase
33
from django.utils.datetime_safe import datetime
44
from parameterized import parameterized
5+
from wagtail.core.blocks import PageChooserBlock
56
from wagtail.core.blocks import StreamBlock
67
from wagtail.core.blocks import StreamValue
78
from wagtailmarkdown.blocks import MarkdownBlock
@@ -27,8 +28,8 @@ def test_that_new_article_page_can_be_only_index_blog_page_child(self):
2728
)
2829
def test_that_new_article_page_should_has_all_mandatory_parameters(self, parameter_name, parameter_value):
2930
author = BossFactory()
30-
block = StreamBlock([("markdown", MarkdownBlock())])
31-
body = StreamValue(block, [("markdown", "Hello, World")])
31+
body_block = StreamBlock([("markdown", MarkdownBlock())])
32+
body = StreamValue(body_block, [("markdown", "Hello, World")])
3233
blog_article_parameter = {
3334
"title": "Simple Article Title",
3435
"date": datetime.now(),
@@ -67,3 +68,42 @@ def test_that_title_should_not_be_longer_than_custom_specified_amount_of_charact
6768
title_that_is_way_too_long = "f" * (MAX_BLOG_ARTICLE_TITLE_LENGTH + 1)
6869
with self.assertRaises(ValidationError):
6970
self._create_blog_article_page(title=title_that_is_way_too_long)
71+
72+
def test_that_recommended_articles_should_not_contain_duplicates(self):
73+
other_article = self._create_blog_article_page()
74+
articles_block = StreamBlock([("page", PageChooserBlock())])
75+
recommended_articles_with_duplicates = StreamValue(
76+
articles_block, [("page", other_article), ("page", other_article)]
77+
)
78+
with self.assertRaises(ValidationError):
79+
self._create_blog_article_page(recommended_articles=recommended_articles_with_duplicates)
80+
81+
def test_that_existing_article_should_not_be_given_duplicated_article_in_recommendations(self):
82+
other_article = self._create_blog_article_page()
83+
articles_block = StreamBlock([("page", PageChooserBlock())])
84+
recommended_articles = StreamValue(articles_block, [("page", other_article)])
85+
article = self._create_blog_article_page(recommended_articles=recommended_articles)
86+
article.recommended_articles.stream_data.append(("page", other_article))
87+
with self.assertRaises(ValidationError):
88+
article.full_clean()
89+
90+
def test_that_article_with_no_recommended_articles_should_be_valid(self):
91+
author = BossFactory()
92+
body_block = StreamBlock([("markdown", MarkdownBlock())])
93+
body = StreamValue(body_block, [("markdown", "Hello, World")])
94+
blog_article_parameter = {
95+
"title": "Simple Article Title",
96+
"date": datetime.now(),
97+
"intro": "Simple Article Intro",
98+
"body": body,
99+
"author": author,
100+
"read_time": 7,
101+
"views": 0,
102+
}
103+
articles_block = StreamBlock([("page", PageChooserBlock())])
104+
105+
blog_article_page = BlogArticlePage(**blog_article_parameter)
106+
self.blog_index_page.add_child(instance=blog_article_page)
107+
blog_article_page.save()
108+
109+
self.assertEqual(blog_article_page.recommended_articles, StreamValue(articles_block, []))

blog/tests/test_helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def _create_blog_article_page(
4949
body=None,
5050
author=None,
5151
read_time=7,
52+
recommended_articles=None,
5253
views=0,
5354
cover_photo=None,
5455
article_photo=None,
@@ -66,6 +67,7 @@ def _create_blog_article_page(
6667
body=body,
6768
author=author,
6869
read_time=read_time,
70+
recommended_articles=recommended_articles,
6971
views=views,
7072
cover_photo=cover_photo,
7173
article_photo=article_photo,

project_liberation/management/commands/load_initial_data.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.db import transaction
77
from django.utils.datetime_safe import datetime
88
from faker import Faker
9+
from wagtail.core.blocks import PageChooserBlock
910
from wagtail.core.blocks import StreamBlock
1011
from wagtail.core.blocks import StreamValue
1112
from wagtail.core.models import Page
@@ -90,13 +91,23 @@ def handle(self, *args, **options):
9091

9192
employees = Employees.objects.all()
9293
# Create articles
94+
last_id = 0
9395
for article_number in range(0, self.articles_limit):
9496
blog_index_page = BlogIndexPage.objects.get(**blog_index_page_parameters)
9597
# base article parameters
9698
index = article_number % employees.count()
9799
author = employees[index]
98100
block = StreamBlock([("markdown", MarkdownBlock())])
99101
body = StreamValue(block, [("markdown", fake.sentence(nb_words=1000))])
102+
# recommended articles
103+
if article_number > 3:
104+
articles_block = StreamBlock([("page", PageChooserBlock())])
105+
articles_data = []
106+
for i in range(3):
107+
articles_data.append(("page", BlogArticlePage.objects.get(id=(last_id - i))))
108+
recommended_articles = StreamValue(articles_block, articles_data)
109+
else:
110+
recommended_articles = None
100111
# article images
101112
rgb_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
102113
wagtail_cover_photo = self._generate_wagtail_image(
@@ -113,13 +124,15 @@ def handle(self, *args, **options):
113124
body=body,
114125
author=author,
115126
read_time=random.randint(1, 10),
127+
recommended_articles=recommended_articles,
116128
views=0,
117129
cover_photo=wagtail_cover_photo,
118130
article_photo=wagtail_article_photo,
119131
is_main_article=True,
120132
)
121133
blog_index_page.add_child(instance=blog_article_page)
122134
blog_article_page.save()
135+
last_id = blog_article_page.id
123136

124137
@staticmethod
125138
def _generate_wagtail_image(resolution: Dict[str, int], name: str, rgb_color=None) -> WagtailImage:

0 commit comments

Comments
 (0)