Skip to content

Commit d2b92dc

Browse files
istridesheralim012Alihassanc5
authored
Add media URLs to Page Tree Api responses for offline access (#1612)
Co-authored-by: Sher Ali <[email protected]> Co-authored-by: Ali Hassan <[email protected]>
1 parent 0fa6655 commit d2b92dc

File tree

6 files changed

+139
-92
lines changed

6 files changed

+139
-92
lines changed

home/mixins.py

-35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
from bs4 import BeautifulSoup
21
from django.utils.functional import cached_property
3-
from wagtail.images.models import Rendition
42

53

64
class PageUtilsMixin:
@@ -32,39 +30,6 @@ def is_first_content(self):
3230
def get_type(self):
3331
return self.__class__.__name__.lower()
3432

35-
def _get_renditions(self, image_id):
36-
image_urls = []
37-
for rendition in Rendition.objects.filter(image_id=image_id):
38-
image_urls.append(rendition.url)
39-
return image_urls
40-
41-
def _get_images_from_block(self, value):
42-
image_urls = []
43-
44-
tags = BeautifulSoup(value, "html.parser").find_all('embed')
45-
for tag in tags:
46-
if tag.attrs['embedtype'] == 'image':
47-
image_urls += self._get_renditions(tag.attrs['id'])
48-
49-
return image_urls
50-
51-
def _get_stream_data_image_urls(self, raw_data):
52-
image_urls = []
53-
54-
for block in raw_data:
55-
if block['type'] == 'image':
56-
image_urls += self._get_renditions(block['value'])
57-
if block['type'] == 'paragraph':
58-
image_urls += self._get_images_from_block(block['value'])
59-
if block['type'] == 'download':
60-
image_urls += self._get_images_from_block(block['value'].get('description', ''))
61-
62-
return image_urls
63-
64-
@property
65-
def get_image_urls(self):
66-
raise NotImplementedError
67-
6833

6934
class TitleIconMixin:
7035
"""

home/models.py

+22-27
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,32 @@
3030
from wagtail.contrib.settings.models import BaseSetting
3131
from wagtail.contrib.settings.registry import register_setting
3232
from wagtail.core import blocks
33-
from wagtail.core.fields import StreamField, RichTextField
33+
from wagtail.core.fields import StreamField
3434
from wagtail.core.models import Orderable, Page, Site, Locale
3535
from wagtail.core.rich_text import get_text_for_indexing
3636
from wagtail.images.blocks import ImageChooserBlock
3737
from wagtail.images.edit_handlers import ImageChooserPanel
3838
from wagtail.images.models import Image
3939
from wagtail.search import index
4040
from wagtailmarkdown.blocks import MarkdownBlock
41-
from wagtailmenus.models import AbstractFlatMenuItem, BooleanField
41+
from wagtailmenus.models import AbstractFlatMenuItem
4242
from wagtailsvg.models import Svg
4343
from wagtailsvg.edit_handlers import SvgChooserPanel
4444

4545
from messaging.blocks import ChatBotButtonBlock
46-
from comments.models import CommentableMixin, CannedResponse
47-
from .blocks import (
46+
from comments.models import CommentableMixin
47+
from home.blocks import (
4848
MediaBlock, SocialMediaLinkBlock, SocialMediaShareButtonBlock, EmbeddedPollBlock, EmbeddedSurveyBlock,
4949
EmbeddedQuizBlock, PageButtonBlock, NumberedListBlock, RawHTMLBlock, ArticleBlock, DownloadButtonBlock,
5050
)
5151
from .forms import SectionPageForm
5252
from .mixins import PageUtilsMixin, TitleIconMixin
5353
from .utils.image import convert_svg_to_png_bytes
5454
from .utils.progress_manager import ProgressManager
55+
from home.utils import (
56+
collect_urls_from_streamfield,
57+
get_all_renditions_urls,
58+
)
5559
import iogt.iogt_globals as globals_
5660

5761
User = get_user_model()
@@ -91,8 +95,8 @@ def get_context(self, request):
9195
return context
9296

9397
@property
94-
def get_image_urls(self):
95-
return self._get_stream_data_image_urls(self.home_featured_content.raw_data)
98+
def offline_urls(self):
99+
return [self.url] + collect_urls_from_streamfield(self.home_featured_content)
96100

97101

98102
class FeaturedContent(Orderable):
@@ -262,18 +266,16 @@ def get_progress_bar_eligible_sections():
262266
return Section.objects.exclude(pk__in=all_descendants)
263267

264268
@property
265-
def get_image_urls(self):
266-
image_urls = []
269+
def offline_urls(self):
270+
urls = [self.url] + collect_urls_from_streamfield(self.body)
267271

268272
if self.lead_image:
269-
image_urls += self._get_renditions(self.lead_image)
273+
urls += get_all_renditions_urls(self.lead_image)
270274

271275
if self.image_icon:
272-
image_urls += self._get_renditions(self.image_icon)
273-
274-
image_urls += self._get_stream_data_image_urls(self.body.raw_data)
276+
urls += get_all_renditions_urls(self.image_icon)
275277

276-
return image_urls
278+
return urls
277279

278280
class Meta:
279281
verbose_name = _("section")
@@ -405,18 +407,16 @@ def top_level_section(self):
405407
return self.get_ancestors().filter(depth=4).first().specific
406408

407409
@property
408-
def get_image_urls(self):
409-
image_urls = []
410+
def offline_urls(self):
411+
urls = [self.url] + collect_urls_from_streamfield(self.body)
410412

411413
if self.lead_image:
412-
image_urls += self._get_renditions(self.lead_image)
414+
urls += get_all_renditions_urls(self.lead_image)
413415

414416
if self.image_icon:
415-
image_urls += self._get_renditions(self.image_icon)
416-
417-
image_urls += self._get_stream_data_image_urls(self.body.raw_data)
417+
urls += get_all_renditions_urls(self.image_icon)
418418

419-
return image_urls
419+
return urls
420420

421421
class Meta:
422422
abstract = True
@@ -505,13 +505,8 @@ class BannerPage(Page, PageUtilsMixin):
505505
]
506506

507507
@property
508-
def get_image_urls(self):
509-
image_urls = []
510-
511-
if self.banner_image:
512-
image_urls += self._get_renditions(self.banner_image)
513-
514-
return image_urls
508+
def offline_urls(self):
509+
return get_all_renditions_urls(self.banner_image)
515510

516511

517512
class FooterIndexPage(Page):

home/utils/__init__.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from bs4 import BeautifulSoup
2+
3+
4+
def get_all_renditions_urls(image):
5+
return [
6+
rendition.url
7+
for rendition
8+
in image.get_rendition_model().objects.filter(image_id=image.id)
9+
]
10+
11+
12+
def extract_urls_from_rich_text(text):
13+
return [
14+
img['src']
15+
for img
16+
in BeautifulSoup(str(text), 'lxml').find_all('img')
17+
]
18+
19+
20+
def collect_urls_from_streamfield(field):
21+
urls = []
22+
for block in field:
23+
if block.block_type == 'image':
24+
urls += get_all_renditions_urls(block.value)
25+
if block.block_type == 'paragraph':
26+
urls += extract_urls_from_rich_text(block.value)
27+
if block.block_type == 'download':
28+
urls += extract_urls_from_rich_text(
29+
block.value.get('description', '')
30+
)
31+
if block.block_type == 'media':
32+
urls.append(block.value.url)
33+
return urls

iogt/tests.py

+51-15
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
from django.urls import reverse
55
from rest_framework import status
66
from wagtail.core.models import Site
7+
from wagtail.core.rich_text import RichText
78
from wagtail_factories import ImageFactory, SiteFactory
8-
from wagtail.images.models import Image
99

1010
from home.factories import (
11-
SectionFactory,
1211
ArticleFactory,
1312
HomePageFactory,
14-
SVGToPNGMapFactory,
1513
LocaleFactory,
14+
MediaFactory,
15+
MiscellaneousIndexPageFactory,
1616
OfflineContentIndexPageFactory,
17-
MiscellaneousIndexPageFactory
17+
SectionFactory,
18+
SVGToPNGMapFactory,
1819
)
1920
from iogt.utils import has_md5_hash
2021

@@ -33,12 +34,11 @@ def setUp(self):
3334
self.en_offline_content_index_page = OfflineContentIndexPageFactory(parent=self.en_miscellaneous_index_page)
3435
self.ar_offline_content_index_page = OfflineContentIndexPageFactory(parent=self.ar_miscellaneous_index_page)
3536
self.section = SectionFactory(parent=self.en_home_page)
36-
self.article = ArticleFactory(parent=self.en_home_page, body=[("image", ImageFactory())])
37-
self.section_lead_image_rendition = self.section.lead_image.get_rendition('fill-360x360')
38-
self.article_lead_image_rendition = self.article.lead_image.get_rendition('fill-360x360')
39-
self.article_body_image_rendition = Image.objects.get(
40-
id=self.article.body.raw_data[0]['value']).get_rendition('fill-360x360')
41-
self.svg_to_png_map = SVGToPNGMapFactory()
37+
self.article = ArticleFactory(
38+
parent=self.en_home_page,
39+
body=[("image", ImageFactory()),
40+
("media", MediaFactory(type='video')),
41+
("media", MediaFactory(type='audio'))])
4242

4343
def test_root_page_is_returned(self):
4444
response = self.client.get(reverse(self.url_name, kwargs={'page_id': self.en_home_page.id}))
@@ -54,13 +54,45 @@ def test_descendants_are_returned(self):
5454
self.assertIn(self.article.url, response.data)
5555

5656
def test_images_are_returned(self):
57-
response = self.client.get(reverse(self.url_name, kwargs={'page_id': self.en_home_page.id}))
57+
svg_to_png_map = SVGToPNGMapFactory()
58+
section_lead_image = get_rendition(self.section.lead_image)
59+
article_lead_image = get_rendition(self.article.lead_image)
60+
body_image = get_rendition(self.article.body[0].value)
61+
62+
response = self.client.get(
63+
reverse(self.url_name, kwargs={'page_id': self.en_home_page.id})
64+
)
5865

5966
self.assertEqual(response.status_code, status.HTTP_200_OK)
60-
self.assertIn(self.section_lead_image_rendition.url, response.data)
61-
self.assertIn(self.article_lead_image_rendition.url, response.data)
62-
self.assertIn(self.article_body_image_rendition.url, response.data)
63-
self.assertIn(self.svg_to_png_map.url, response.data)
67+
self.assertIn(section_lead_image.url, response.data)
68+
self.assertIn(article_lead_image.url, response.data)
69+
self.assertIn(body_image.url, response.data)
70+
self.assertIn(svg_to_png_map.url, response.data)
71+
72+
def test_media_are_returned(self):
73+
response = self.client.get(
74+
reverse(self.url_name, kwargs={'page_id': self.en_home_page.id})
75+
)
76+
77+
self.assertEqual(response.status_code, status.HTTP_200_OK)
78+
self.assertIn(self.article.body[1].value.url, response.data)
79+
self.assertIn(self.article.body[2].value.url, response.data)
80+
81+
def test_image_urls_extracted_from_rich_text(self):
82+
image = ImageFactory()
83+
source = f'<embed embedtype="image" format="left" id="{image.id}"/>'
84+
ArticleFactory(
85+
parent=self.en_home_page,
86+
body=[('paragraph', RichText(source))]
87+
)
88+
response = self.client.get(
89+
reverse(self.url_name, kwargs={'page_id': self.en_home_page.id})
90+
)
91+
self.assertEqual(response.status_code, status.HTTP_200_OK)
92+
93+
renditions = image.get_rendition_model().objects.filter(image_id=image.id)
94+
self.assertEquals(len(renditions), 1)
95+
self.assertIn(renditions[0].url, response.data)
6496

6597
def test_static_assets_are_returned(self):
6698
response = self.client.get(reverse(self.url_name, kwargs={'page_id': self.en_home_page.id}))
@@ -110,3 +142,7 @@ def test_has_md5_hash_without_has_values(self):
110142

111143
for value in without_md5_hash:
112144
self.assertFalse(has_md5_hash(value))
145+
146+
147+
def get_rendition(image):
148+
return image.get_rendition('fill-360x360')

iogt/views.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,22 @@ def get(self, request, page_id):
120120
if page:
121121
page_urls.append(page.url)
122122
translation.activate(active_locale.language_code)
123-
image_urls = []
123+
124+
no_impl = set()
124125
for page in pages:
125-
if isinstance(page, (HomePage, Section, Article, Poll, Survey, Quiz)):
126-
page_urls.append(page.url)
127-
image_urls += page.get_image_urls
126+
try:
127+
page_urls += page.offline_urls
128+
except AttributeError:
129+
no_impl.add(type(page).__name__)
130+
131+
if no_impl:
132+
types = ", ".join(sorted(no_impl))
133+
logger.warn(
134+
f"Offline URLs could not be obtained for the following Page types: "
135+
f"{types}"
136+
)
128137

138+
image_urls = []
129139
for svg_to_png_map in SVGToPNGMap.objects.all():
130140
image_urls.append(svg_to_png_map.url)
131141

questionnaires/models.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@
1717
from wagtailsvg.edit_handlers import SvgChooserPanel
1818
from wagtailsvg.models import Svg
1919

20-
from home.blocks import MediaBlock, PageButtonBlock, NumberedListBlock, RawHTMLBlock
20+
from home.blocks import (
21+
MediaBlock,
22+
NumberedListBlock,
23+
PageButtonBlock,
24+
RawHTMLBlock,
25+
)
2126
from home.mixins import PageUtilsMixin, TitleIconMixin
27+
from home.utils import (
28+
collect_urls_from_streamfield,
29+
get_all_renditions_urls,
30+
)
2231
from modelcluster.fields import ParentalKey
2332
from wagtail.admin.edit_handlers import (FieldPanel, InlinePanel,
2433
MultiFieldPanel, StreamFieldPanel)
@@ -235,8 +244,6 @@ def serve_questions_separately(self, request, *args, **kwargs):
235244
return render(request, self.template, context)
236245

237246
def process_form_submission(self, form):
238-
from home.models import SiteSettings
239-
240247
user = form.user
241248
self.get_submission_class().objects.create(
242249
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
@@ -270,16 +277,17 @@ def get_data_fields(self):
270277
return data_fields
271278

272279
@property
273-
def get_image_urls(self):
274-
image_urls = []
280+
def offline_urls(self):
281+
urls = (
282+
[self.url]
283+
+ collect_urls_from_streamfield(self.description)
284+
+ collect_urls_from_streamfield(self.thank_you_text)
285+
)
275286

276287
if self.image_icon:
277-
image_urls += self._get_renditions(self.image_icon)
278-
279-
image_urls += self._get_stream_data_image_urls(self.description.raw_data)
280-
image_urls += self._get_stream_data_image_urls(self.thank_you_text.raw_data)
288+
urls += get_all_renditions_urls(self.image_icon)
281289

282-
return image_urls
290+
return urls
283291

284292
def get_submit_button_text(self, fields_step=None):
285293
submit_button_text = self.submit_button_text
@@ -633,7 +641,7 @@ def get_results(self, data=None):
633641
results = dict()
634642
results_list = []
635643
field = self.get_form_fields().first()
636-
name, label, choices = field.clean_name, field.label, field.choices
644+
name, choices = field.clean_name, field.choices
637645
submissions = self.get_submission_class().objects.filter(page=self)
638646
submissions = [submission for submission in submissions if submission.get_data().get(name) != '']
639647

0 commit comments

Comments
 (0)