diff --git a/backend/settings/settings.py b/backend/settings/settings.py
index 276f96f739..a675226fbd 100644
--- a/backend/settings/settings.py
+++ b/backend/settings/settings.py
@@ -19,6 +19,10 @@
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
+# On deployed environments, Django needs to access the index.html template
+# built separately by the frontend toolchain.
+FRONTEND_STATIC_FILES_PATH = Path("/srv/tournesol-frontend")
+
load_dotenv()
server_settings = {}
@@ -71,6 +75,7 @@
MEDIA_ROOT = server_settings.get("MEDIA_ROOT", f"{base_folder}{MEDIA_URL}")
MAIN_URL = server_settings.get("MAIN_URL", "http://localhost:8000/")
+TOURNESOL_MAIN_URL = server_settings.get("TOURNESOL_MAIN_URL", "http://localhost:3000/")
TOURNESOL_VERSION = server_settings.get("TOURNESOL_VERSION", "")
@@ -95,6 +100,7 @@
"drf_spectacular",
"rest_registration",
"vouch",
+ "ssr",
]
# Workaround for tests using TransactionTestCase with `serialized_rollback=True`
@@ -102,9 +108,7 @@
# See bug https://code.djangoproject.com/ticket/30751
TEST_NON_SERIALIZED_APPS = ["django.contrib.contenttypes", "django.contrib.auth"]
-REST_REGISTRATION_MAIN_URL = server_settings.get(
- "REST_REGISTRATION_MAIN_URL", "http://localhost:3000/"
-)
+REST_REGISTRATION_MAIN_URL = TOURNESOL_MAIN_URL
REST_REGISTRATION = {
"REGISTER_VERIFICATION_ENABLED": True,
"REGISTER_VERIFICATION_URL": REST_REGISTRATION_MAIN_URL + "verify-user/",
@@ -170,6 +174,7 @@
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_prometheus.middleware.PrometheusAfterMiddleware",
]
+X_FRAME_OPTIONS = "SAMEORIGIN"
ROOT_URLCONF = "settings.urls"
diff --git a/backend/settings/urls.py b/backend/settings/urls.py
index 80f019d34b..005599721c 100644
--- a/backend/settings/urls.py
+++ b/backend/settings/urls.py
@@ -48,4 +48,5 @@
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
+ path("ssr/", include("ssr.urls")),
]
diff --git a/backend/ssr/__init__.py b/backend/ssr/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/ssr/apps.py b/backend/ssr/apps.py
new file mode 100644
index 0000000000..59f6c9c9a0
--- /dev/null
+++ b/backend/ssr/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class SsrConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "ssr"
diff --git a/backend/ssr/templates/opengraph/meta_tags.html b/backend/ssr/templates/opengraph/meta_tags.html
new file mode 100644
index 0000000000..095061d6bd
--- /dev/null
+++ b/backend/ssr/templates/opengraph/meta_tags.html
@@ -0,0 +1,3 @@
+{% for key, value in meta_tags.items %}
+
+{% endfor %}
\ No newline at end of file
diff --git a/backend/ssr/tests.py b/backend/ssr/tests.py
new file mode 100644
index 0000000000..fd7ae40ca5
--- /dev/null
+++ b/backend/ssr/tests.py
@@ -0,0 +1,46 @@
+from unittest.mock import patch
+
+from django.test import Client, TestCase
+
+from tournesol.tests.factories.entity import VideoFactory
+
+
+def mock_get_static_index_html():
+ return """
+
+
+
+ Tournesol
+
+
+
+ Mocked html page
+
+
+ """
+
+
+@patch("ssr.views.get_static_index_html", new=mock_get_static_index_html)
+class RenderedHtmlTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+
+ def test_index_html_root(self):
+ response = self.client.get("/ssr/")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '')
+
+ def test_index_html_arbitrary_path(self):
+ response = self.client.get("/ssr/faq")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, '')
+
+ def test_index_html_video_entity(self):
+ video = VideoFactory()
+
+ response = self.client.get(f"/ssr/entities/{video.uid}")
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(
+ response, f''
+ )
+ self.assertContains(response, '')
diff --git a/backend/ssr/urls.py b/backend/ssr/urls.py
new file mode 100644
index 0000000000..85320c500e
--- /dev/null
+++ b/backend/ssr/urls.py
@@ -0,0 +1,17 @@
+from django.urls import path, re_path
+
+from . import views
+
+
+urlpatterns = [
+ path(
+ "entities/",
+ views.render_tournesol_html_with_dynamic_tags,
+ name="ssr_entities",
+ ),
+ re_path(
+ r".*",
+ views.render_tournesol_html_with_dynamic_tags,
+ name="ssr_default",
+ ),
+]
diff --git a/backend/ssr/views.py b/backend/ssr/views.py
new file mode 100644
index 0000000000..46c5e5de61
--- /dev/null
+++ b/backend/ssr/views.py
@@ -0,0 +1,80 @@
+import typing as tp
+
+import requests
+from django.http import HttpResponse, HttpRequest
+from django.conf import settings
+from django.template.loader import render_to_string
+
+from tournesol.models import Entity
+from tournesol.models.entity import TYPE_VIDEO
+
+
+def get_static_index_html() -> str:
+ if settings.DEBUG:
+ try:
+ # Try to get index.html from dev-env frontend container
+ resp = requests.get("http://tournesol-dev-front:3000")
+ except requests.ConnectionError:
+ resp = requests.get(settings.TOURNESOL_MAIN_URL)
+ resp.raise_for_status()
+ return resp.text
+ return (settings.FRONTEND_STATIC_FILES_PATH / "index.html").read_text()
+
+
+def get_default_meta_tags(request: HttpRequest) -> dict[str, str]:
+ full_frontend_path = request.get_full_path().removeprefix("/ssr/")
+ return {
+ "og:site_name": "Tournesol",
+ "og:type": "website",
+ "og:title": "Tournesol",
+ "og:description": (
+ "Compare online content and contribute to the development of "
+ "responsible content recommendations."
+ ),
+ "og:image": f"{settings.MAIN_URL}preview/{full_frontend_path}",
+ "og:url": f"{settings.TOURNESOL_MAIN_URL}{full_frontend_path}",
+ "twitter:card": "summary_large_image",
+ }
+
+
+def get_entity_meta_tags(uid: str) -> dict[str, str]:
+ try:
+ entity: Entity = Entity.objects.get(uid=uid)
+ except Entity.DoesNotExist:
+ return {}
+
+ if entity.type != TYPE_VIDEO:
+ return {}
+
+ meta_tags = {
+ "og:type": "video",
+ "og:video:url": f"https://youtube.com/embed/{entity.video_id}",
+ "og:video:type": "text/html",
+ }
+
+ if video_title := entity.metadata.get("name"):
+ meta_tags["og:title"] = video_title
+
+ if video_channel_name := entity.metadata.get("uploader"):
+ meta_tags["og:description"] = video_channel_name
+
+ return meta_tags
+
+
+def render_tournesol_html_with_dynamic_tags(request: HttpRequest, uid: tp.Optional[str] = None):
+ index_html = get_static_index_html()
+ meta_tags = get_default_meta_tags(request)
+ if uid is not None:
+ meta_tags |= get_entity_meta_tags(uid)
+
+ rendered_html = index_html.replace(
+ "",
+ render_to_string(
+ "opengraph/meta_tags.html",
+ {
+ "meta_tags": meta_tags,
+ },
+ ),
+ 1,
+ )
+ return HttpResponse(rendered_html)
diff --git a/backend/tournesol/models/entity.py b/backend/tournesol/models/entity.py
index c31c6c1044..8f9230ee51 100644
--- a/backend/tournesol/models/entity.py
+++ b/backend/tournesol/models/entity.py
@@ -312,7 +312,7 @@ def link_to_tournesol(self):
return None
video_uri = urljoin(
- settings.REST_REGISTRATION_MAIN_URL, f"entities/yt:{self.video_id}"
+ settings.TOURNESOL_MAIN_URL, f"entities/yt:{self.video_id}"
)
return format_html('Play ▶', video_uri)
diff --git a/backend/tournesol/resources/Poppins-Bold.ttf b/backend/tournesol/resources/Poppins-Bold.ttf
new file mode 100644
index 0000000000..00559eeb29
Binary files /dev/null and b/backend/tournesol/resources/Poppins-Bold.ttf differ
diff --git a/backend/tournesol/tests/test_api_preview.py b/backend/tournesol/tests/test_api_preview.py
index 907ea85bf0..202c0724f6 100644
--- a/backend/tournesol/tests/test_api_preview.py
+++ b/backend/tournesol/tests/test_api_preview.py
@@ -1,5 +1,6 @@
from unittest.mock import patch
+import requests
from django.test import TestCase
from PIL import Image
from requests import Response
@@ -11,14 +12,14 @@
from tournesol.entities.video import TYPE_VIDEO
from tournesol.models import Entity, EntityPollRating
-from .factories.entity import EntityFactory, VideoCriteriaScoreFactory, VideoFactory
+from .factories.entity import VideoCriteriaScoreFactory, VideoFactory
def raise_(exception):
raise exception
-def mock_yt_thumbnail_response(url, timeout=None) -> Response:
+def mock_yt_thumbnail_response(self, url, timeout=None) -> Response:
resp = Response()
resp.status_code = 200
resp._content = Image.new("1", (1, 1)).tobitmap()
@@ -104,7 +105,7 @@ def setUp(self):
self.preview_url = "/preview/entities/"
self.valid_uid = "yt:sDPk-r18sb0"
- @patch("requests.get", mock_yt_thumbnail_response)
+ @patch.object(requests.Session, "get", mock_yt_thumbnail_response)
@patch("tournesol.entities.video.VideoEntity.update_search_vector", lambda x: None)
def test_auth_200_get(self):
"""
@@ -131,7 +132,7 @@ def test_auth_200_get(self):
# check is not very robust.
self.assertNotIn("Content-Disposition", response.headers)
- @patch("requests.get", mock_yt_thumbnail_response)
+ @patch.object(requests.Session, "get", mock_yt_thumbnail_response)
@patch("tournesol.entities.video.VideoEntity.update_search_vector", lambda x: None)
def test_anon_200_get_existing_entity(self):
"""
@@ -188,7 +189,7 @@ def test_get_preview_no_duration(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.headers["Content-Type"], "image/jpeg")
- @patch("requests.get", lambda x, timeout=None: raise_(ConnectionError))
+ @patch.object(requests.Session, "get", lambda *args, **kwargs: raise_(ConnectionError))
@patch("tournesol.entities.video.VideoEntity.update_search_vector", lambda x: None)
def test_anon_200_get_with_yt_connection_error(self):
"""
@@ -231,7 +232,7 @@ def setUp(self):
self.valid_uid = "yt:sDPk-r18sb0"
self.valid_uid2 = "yt:VKsekCHBuHI"
- @patch("requests.get", mock_yt_thumbnail_response)
+ @patch.object(requests.Session, "get", mock_yt_thumbnail_response)
@patch("tournesol.entities.video.VideoEntity.update_search_vector", lambda x: None)
def test_auth_200_get(self):
"""
@@ -279,7 +280,7 @@ def test_auth_200_get(self):
self.assertEqual(response.headers["Content-Type"], "image/jpeg")
self.assertNotIn("Content-Disposition", response.headers)
- @patch("requests.get", mock_yt_thumbnail_response)
+ @patch.object(requests.Session, "get", mock_yt_thumbnail_response)
@patch("tournesol.entities.video.VideoEntity.update_search_vector", lambda x: None)
def test_anon_200_get_existing_entities(self):
"""
@@ -412,7 +413,7 @@ def test_anon_200_get_invalid_entity_type(self):
'inline; filename="tournesol_screenshot_og.png"',
)
- @patch("requests.get", lambda x, timeout=None: raise_(ConnectionError))
+ @patch.object(requests.Session, "get", lambda *args, **kwargs: raise_(ConnectionError))
@patch("tournesol.entities.video.VideoEntity.update_search_vector", lambda x: None)
def test_anon_200_get_with_yt_connection_error(self):
"""
diff --git a/backend/tournesol/views/previews/default.py b/backend/tournesol/views/previews/default.py
index e5c1d7e7ca..652445ede1 100644
--- a/backend/tournesol/views/previews/default.py
+++ b/backend/tournesol/views/previews/default.py
@@ -32,27 +32,26 @@
CACHE_DEFAULT_PREVIEW = 3600 * 24 # 24h
CACHE_ENTITY_PREVIEW = 3600 * 2
+POPPINS_BOLD_LOCATION = "tournesol/resources/Poppins-Bold.ttf"
FOOTER_FONT_LOCATION = "tournesol/resources/Poppins-Medium.ttf"
LIGHT_FONT_LOCATION = "tournesol/resources/Poppins-Light.ttf"
LIGHT_ITALIC_FONT_LOCATION = "tournesol/resources/Poppins-LightItalic.ttf"
REGULAR_FONT_LOCATION = "tournesol/resources/Poppins-Regular.ttf"
DURATION_FONT_LOCATION = "tournesol/resources/Roboto-Bold.ttf"
-ENTITY_N_CONTRIBUTORS_XY = (60, 98)
-ENTITY_TITLE_XY = (128, 194)
-
-TOURNESOL_SCORE_XY = (84, 30)
-TOURNESOL_SCORE_UNSAFE_XY = (60, 30)
COLOR_YELLOW_BORDER = (255, 200, 0, 255)
-COLOR_YELLOW_BACKGROUND = (255, 200, 0, 16)
COLOR_WHITE_BACKGROUND = (255, 250, 230, 255)
-COLOR_BROWN_FONT = (29, 26, 20, 255)
+
COLOR_WHITE_FONT = (255, 255, 255, 255)
+COLOR_YELLOW_FONT = (255, 200, 0, 255)
+COLOR_BROWN_FONT = (29, 26, 20, 255)
COLOR_GREY_FONT = (160, 155, 135, 255)
-COLOR_UNSAFE_SCORE = (128, 128, 128, 248)
+COLOR_UNSAFE_SCORE = (180, 180, 180, 248)
COLOR_DURATION_RECTANGLE = (0, 0, 0, 201)
-YT_THUMBNAIL_MQ_SIZE = (320, 180)
+PREVIEW_BASE_SIZE = (440, 240)
+
+session = requests.Session()
class BasePreviewAPIView(APIView):
@@ -102,7 +101,7 @@ def get_yt_thumbnail(
# Quality can be: hq, mq, sd, or maxres (https://stackoverflow.com/a/34784842/188760)
url = f"https://img.youtube.com/vi/{entity.video_id}/{quality}default.jpg"
try:
- thumbnail_response = requests.get(url, timeout=REQUEST_TIMEOUT)
+ thumbnail_response = session.get(url, timeout=REQUEST_TIMEOUT)
except (ConnectionError, Timeout) as exc:
logger.error("Preview failed for entity with UID %s.", entity.uid)
logger.error("Exception caught: %s", exc)
@@ -151,8 +150,8 @@ def get_best_quality_yt_thumbnail(self, entity: Entity) -> Optional[Image.Image]
def get_preview_font_config(upscale_ratio=1) -> dict:
config = {
- "ts_score": ImageFont.truetype(
- str(BASE_DIR / FOOTER_FONT_LOCATION), 32 * upscale_ratio
+ "entity_preview_ts_score": ImageFont.truetype(
+ str(BASE_DIR / POPPINS_BOLD_LOCATION), 32 * upscale_ratio
),
"entity_title": ImageFont.truetype(
str(BASE_DIR / FOOTER_FONT_LOCATION), 14 * upscale_ratio
@@ -218,105 +217,6 @@ def font_height(font):
return ascent + descent
-def get_preview_frame(entity: Entity, fnt_config, upscale_ratio=1) -> Image.Image:
- tournesol_frame = Image.new(
- "RGBA", (440 * upscale_ratio, 240 * upscale_ratio), COLOR_WHITE_BACKGROUND
- )
- tournesol_frame_draw = ImageDraw.Draw(tournesol_frame)
-
- full_title = entity.metadata.get("name", "")
- truncated_title = truncate_text(
- tournesol_frame_draw,
- full_title,
- font=fnt_config["entity_title"],
- available_width=300 * upscale_ratio,
- )
-
- full_uploader = entity.metadata.get("uploader", "")
- truncated_uploader = truncate_text(
- tournesol_frame_draw,
- full_uploader,
- font=fnt_config["entity_title"],
- available_width=300 * upscale_ratio,
- )
-
- tournesol_frame_draw.text(
- tuple(numpy.multiply((ENTITY_TITLE_XY), upscale_ratio)),
- truncated_uploader,
- font=fnt_config["entity_uploader"],
- fill=COLOR_BROWN_FONT,
- )
- tournesol_frame_draw.text(
- tuple(numpy.multiply((ENTITY_TITLE_XY[0], ENTITY_TITLE_XY[1] + 18), upscale_ratio)),
- truncated_title,
- font=fnt_config["entity_title"],
- fill=COLOR_BROWN_FONT,
- )
-
- poll_rating = entity.single_poll_rating
- if poll_rating is not None and poll_rating.tournesol_score is not None:
- score = poll_rating.tournesol_score
-
- if poll_rating.is_recommendation_unsafe:
- score_color = COLOR_UNSAFE_SCORE
- score_xy = TOURNESOL_SCORE_UNSAFE_XY
- else:
- score_xy = TOURNESOL_SCORE_XY
- score_color = COLOR_BROWN_FONT
-
- tournesol_frame_draw.text(
- tuple(numpy.multiply(score_xy, upscale_ratio)),
- f"{score:.0f}",
- font=fnt_config["ts_score"],
- fill=score_color,
- anchor="mt",
- )
- x_coordinate, y_coordinate = ENTITY_N_CONTRIBUTORS_XY
- tournesol_frame_draw.text(
- tuple(numpy.multiply((x_coordinate, y_coordinate), upscale_ratio)),
- f"{poll_rating.n_comparisons if poll_rating else 0}",
- font=fnt_config["entity_ratings"],
- fill=COLOR_BROWN_FONT,
- anchor="mt",
- )
- tournesol_frame_draw.text(
- tuple(numpy.multiply((x_coordinate, y_coordinate + 26), upscale_ratio)),
- "comparisons",
- font=fnt_config["entity_ratings_label"],
- fill=COLOR_BROWN_FONT,
- anchor="mt",
- )
- tournesol_frame_draw.text(
- tuple(numpy.multiply((x_coordinate, y_coordinate + 82), upscale_ratio)),
- f"{poll_rating.n_contributors if poll_rating else 0}",
- font=fnt_config["entity_ratings"],
- fill=COLOR_BROWN_FONT,
- anchor="mt",
- )
- tournesol_frame_draw.text(
- tuple(numpy.multiply((x_coordinate, y_coordinate + 108), upscale_ratio)),
- "contributors",
- font=fnt_config["entity_ratings_label"],
- fill=COLOR_BROWN_FONT,
- anchor="mt",
- )
- tournesol_frame_draw.rectangle(
- (
- tuple(numpy.multiply((114, 0), upscale_ratio)),
- tuple(numpy.multiply((120, 240), upscale_ratio)),
- ),
- fill=COLOR_YELLOW_BORDER,
- )
- tournesol_frame_draw.rectangle(
- (
- tuple(numpy.multiply((120, 180), upscale_ratio)),
- tuple(numpy.multiply((440, 186), upscale_ratio)),
- ),
- fill=COLOR_YELLOW_BORDER,
- )
- return tournesol_frame
-
-
def draw_video_duration(image: Image.Image, entity: Entity, thumbnail_bbox, upscale_ratio: int):
# pylint: disable=too-many-locals
"""
diff --git a/backend/tournesol/views/previews/entities.py b/backend/tournesol/views/previews/entities.py
index d3e81a36b7..23fa5df98c 100644
--- a/backend/tournesol/views/previews/entities.py
+++ b/backend/tournesol/views/previews/entities.py
@@ -1,56 +1,167 @@
"""
API returning preview images of entities.
"""
+
import numpy
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema
-from PIL import Image
+from PIL import Image, ImageDraw
from tournesol.models import Entity
from tournesol.utils.cache import cache_page_no_i18n
from .default import (
CACHE_ENTITY_PREVIEW,
+ COLOR_UNSAFE_SCORE,
+ COLOR_YELLOW_FONT,
+ PREVIEW_BASE_SIZE,
BasePreviewAPIView,
- draw_video_duration,
+ font_height,
get_preview_font_config,
- get_preview_frame,
)
+TS_SCORE_OVERLAY_COLOR = (58, 58, 58, 224)
+TS_SCORE_OVERLAY_MARGIN_B = 0
+TS_SCORE_OVERLAY_PADDING_X = 10
+
+# Shift the score position to the left and to the top, to make space for the logo.
+TS_SCORE_SHIFT_L = 8
+TS_SCORE_SHIFT_T = 2
+
+TS_LOGO_MARGIN = (2, 4)
+TS_LOGO_SIZE = (19, 19)
+
class DynamicWebsitePreviewEntity(BasePreviewAPIView):
"""
- Return a preview of an entity, with its Tournesol score, comparisons and
- contributors.
+ Return a preview of an entity, with its Tournesol score.
"""
permission_classes = []
- def _draw_logo(self, image: Image.Image, entity: Entity, upscale_ratio: int):
+ @staticmethod
+ def draw_score( # pylint: disable=too-many-arguments
+ image: Image.Image,
+ score_overlay: Image.Image,
+ score: float,
+ color: tuple,
+ shift_l: int,
+ shift_t: int,
+ margin_bottom: int,
+ font_config,
+ ):
+ image_draw = ImageDraw.Draw(image)
+ image_draw.text(
+ (
+ image.size[0] - (score_overlay.size[0] / 2) - shift_l,
+ image.size[1] - (score_overlay.size[1] / 2) - shift_t - margin_bottom,
+ ),
+ f"{score:.0f}",
+ font=font_config["entity_preview_ts_score"],
+ fill=color,
+ anchor="mm",
+ )
+
+ @staticmethod
+ def draw_score_overlay(
+ image: Image.Image, size: tuple[int, int], upscale_ratio: int
+ ) -> tuple[Image.Image, int]:
+ score_overlay = Image.new(mode="RGBA", size=tuple(numpy.multiply(size, upscale_ratio)))
+ score_overlay_draw = ImageDraw.Draw(score_overlay)
+ score_overlay_draw.rounded_rectangle(
+ [(0, 0), tuple(numpy.multiply(size, upscale_ratio))],
+ radius=14,
+ fill=TS_SCORE_OVERLAY_COLOR,
+ corners=[True, False, False, False],
+ )
+
+ score_overlay_margin_b = TS_SCORE_OVERLAY_MARGIN_B * upscale_ratio
+
+ image.alpha_composite(
+ im=score_overlay,
+ dest=(
+ image.size[0] - score_overlay.size[0],
+ image.size[1] - score_overlay.size[1] - score_overlay_margin_b,
+ ),
+ )
+
+ return score_overlay, score_overlay_margin_b
+
+ def add_tournesol_score(
+ self, image: Image.Image, entity: Entity, upscale_ratio: int, font_config
+ ):
"""
- Draw the Tournesol logo on the provided image.
+ Draw the entity's Tournesol score on the provided image.
- If the Tournesol score of the entity is negative, nothing is drawn.
+ Non-trusted entities (i.e. with an unsafe rating) are displayed
+ without the Tournesol logo.
"""
- # Negative scores are displayed without the Tournesol logo, to have
- # more space to display the minus symbol, and to make it clear that
- # the entity is not currently trusted by Tournesol.
poll_rating = entity.single_poll_rating
- # If the score has not been computed yet, display a centered flower.
+ # If the score has not been computed yet, display nothing.
if poll_rating is None or poll_rating.tournesol_score is None:
- image.alpha_composite(
- self.get_ts_logo(tuple(numpy.multiply((34, 34), upscale_ratio))),
- dest=tuple(numpy.multiply((43, 24), upscale_ratio)),
- )
+ return
+
+ if poll_rating.is_recommendation_unsafe:
+ score_color = COLOR_UNSAFE_SCORE
+ else:
+ score_color = COLOR_YELLOW_FONT
+
+ shift_l = int(TS_SCORE_SHIFT_L / 2) * upscale_ratio
+ shift_t = int(TS_SCORE_SHIFT_T / 2) * upscale_ratio
+
+ score_overlay_size = (
+ max(
+ int(
+ font_config["entity_preview_ts_score"].getlength(
+ str(int(poll_rating.tournesol_score))
+ )
+ / upscale_ratio
+ )
+ + TS_SCORE_SHIFT_L
+ + TS_SCORE_OVERLAY_PADDING_X,
+ 60,
+ ),
+ int(font_height(font_config["entity_preview_ts_score"]) / upscale_ratio),
+ )
+
+ score_overlay, score_overlay_margin_b = DynamicWebsitePreviewEntity.draw_score_overlay(
+ image=image, size=score_overlay_size, upscale_ratio=upscale_ratio
+ )
+ DynamicWebsitePreviewEntity.draw_score(
+ image=image,
+ score_overlay=score_overlay,
+ score=poll_rating.tournesol_score,
+ color=score_color,
+ shift_l=shift_l,
+ shift_t=shift_t,
+ margin_bottom=score_overlay_margin_b,
+ font_config=font_config,
+ )
- elif not poll_rating.is_recommendation_unsafe:
- image.alpha_composite(
- self.get_ts_logo(tuple(numpy.multiply((34, 34), upscale_ratio))),
- dest=tuple(numpy.multiply((16, 24), upscale_ratio)),
+ if poll_rating.is_recommendation_unsafe:
+ logo = (
+ self.get_ts_logo(tuple(numpy.multiply(TS_LOGO_SIZE, upscale_ratio)))
+ .convert("LA")
+ .convert("RGBA")
)
+ else:
+ logo = self.get_ts_logo(tuple(numpy.multiply(TS_LOGO_SIZE, upscale_ratio)))
+
+ image.alpha_composite(
+ logo,
+ dest=(
+ image.size[0]
+ - TS_LOGO_SIZE[0] * upscale_ratio
+ - TS_LOGO_MARGIN[0] * upscale_ratio,
+ image.size[1]
+ - TS_LOGO_SIZE[1] * upscale_ratio
+ - TS_LOGO_MARGIN[1] * upscale_ratio
+ - score_overlay_margin_b,
+ ),
+ )
@method_decorator(cache_page_no_i18n(CACHE_ENTITY_PREVIEW))
@extend_schema(
@@ -67,27 +178,21 @@ def get(self, request, uid):
return self.default_preview()
upscale_ratio = 2
- fnt_config = get_preview_font_config(upscale_ratio=upscale_ratio)
- preview_image = get_preview_frame(
- entity, fnt_config, upscale_ratio=upscale_ratio
- )
+ font_config = get_preview_font_config(upscale_ratio=upscale_ratio)
+ preview_image = Image.new("RGBA", tuple(numpy.multiply(PREVIEW_BASE_SIZE, upscale_ratio)))
try:
youtube_thumbnail = self.get_best_quality_yt_thumbnail(entity)
except ConnectionError:
return self.default_preview()
- # (width, height, left, top)
- youtube_thumbnail_bbox = tuple(numpy.multiply((320, 180, 120, 0), upscale_ratio))
-
if youtube_thumbnail is not None:
- youtube_thumbnail = youtube_thumbnail.resize(youtube_thumbnail_bbox[0:2])
- preview_image.paste(
- youtube_thumbnail, box=tuple(youtube_thumbnail_bbox[2:4])
- )
+ youtube_thumbnail = youtube_thumbnail.resize(preview_image.size)
+ preview_image.paste(youtube_thumbnail, box=(0, 0))
- draw_video_duration(preview_image, entity, youtube_thumbnail_bbox, upscale_ratio)
- self._draw_logo(preview_image, entity, upscale_ratio=upscale_ratio)
+ self.add_tournesol_score(
+ preview_image, entity, upscale_ratio=upscale_ratio, font_config=font_config
+ )
response = HttpResponse(content_type="image/jpeg")
preview_image.convert("RGB").save(response, "jpeg")
diff --git a/backend/twitterbot/admin.py b/backend/twitterbot/admin.py
index 38223473cf..bb02687c28 100644
--- a/backend/twitterbot/admin.py
+++ b/backend/twitterbot/admin.py
@@ -60,6 +60,6 @@ def get_video_link(obj):
Return the Tournesol front end URI of the video, in the poll `videos`.
"""
video_uri = urljoin(
- settings.REST_REGISTRATION_MAIN_URL, f"entities/yt:{obj.video.video_id}"
+ settings.TOURNESOL_MAIN_URL, f"entities/yt:{obj.video.video_id}"
)
return format_html('Play ▶', video_uri)
diff --git a/frontend/index.html b/frontend/index.html
index d4d043bbe2..609e65ba57 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -24,16 +24,7 @@
name="description"
content="Tournesol is an open source platform which aims to collaboratively identify top videos of public interest by eliciting contributors' judgements on content quality. We hope to contribute to making today's and tomorrow's large-scale algorithms robustly beneficial for all of humanity."
/>
-
-
-
-
-
-
-
-
+