Skip to content

Commit

Permalink
[front] feat: add pages Top Videos, For You and Search (#1984)
Browse files Browse the repository at this point in the history
  • Loading branch information
GresilleSiffle authored Dec 19, 2024
1 parent 801ef15 commit fa3fb06
Show file tree
Hide file tree
Showing 67 changed files with 2,497 additions and 1,157 deletions.
55 changes: 55 additions & 0 deletions backend/core/migrations/0016_migrate_reco_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 4.2.14 on 2024-07-17 12:44

from django.db import migrations


def copy_setting(user, from_key, to_key):
try:
value = user.settings["videos"][from_key]
except KeyError:
return False

user.settings["videos"][to_key] = value
return True


def migrate_reco_settings_forward(apps, schema_editor):
User = apps.get_model("core", "User")
for user in User.objects.iterator():
if "videos" not in user.settings:
continue

# We don't migrate the default_unsafe setting, because we think many
# users would appreciate the default configuration of the feed For you.
copied = map(
copy_setting,
[user] * 5,
[
"recommendations__default_date",
"recommendations__default_languages",
"recommendations__default_exclude_compared_entities",
"recommendations__default_languages",
],
[
"feed_foryou__date",
"feed_foryou__languages",
"feed_foryou__exclude_compared_entities",
"feed_topitems__languages",
],
)

if any(list(copied)):
user.save(update_fields=["settings"])


class Migration(migrations.Migration):

dependencies = [
("core", "0015_use_unlogged_cache_table"),
]

operations = [
migrations.RunPython(
code=migrate_reco_settings_forward, reverse_code=migrations.RunPython.noop
),
]
29 changes: 28 additions & 1 deletion backend/core/serializers/user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ class VideosPollUserSettingsSerializer(GenericPollUserSettingsSerializer):
),
)

# Settings starting with `recommendations__` are deprecated.
#
# They are kept for backward compatibility, mainly for older versions of
# the browser extension. They are replaced by settings dedicated to
# specific recommendation feeds.
recommendations__default_date = serializers.ChoiceField(
choices=DEFAULT_DATE_CHOICES, allow_blank=True, required=False
)
Expand All @@ -103,13 +108,35 @@ class VideosPollUserSettingsSerializer(GenericPollUserSettingsSerializer):
recommendations__default_unsafe = serializers.BooleanField(required=False)
recommendations__default_exclude_compared_entities = serializers.BooleanField(required=False)

def validate_recommendations__default_languages(self, default_languages):
feed_foryou__date = serializers.ChoiceField(
choices=DEFAULT_DATE_CHOICES, allow_blank=True, required=False
)
feed_foryou__languages = serializers.ListField(
child=serializers.CharField(), allow_empty=True, required=False
)
feed_foryou__unsafe = serializers.BooleanField(required=False)
feed_foryou__exclude_compared_entities = serializers.BooleanField(required=False)

feed_topitems__languages = serializers.ListField(
child=serializers.CharField(), allow_empty=True, required=False
)

def _validate_languages(self, default_languages):
for lang in default_languages:
if lang not in ACCEPTED_LANGUAGE_CODES:
raise ValidationError(_("Unknown language code: %(lang)s.") % {"lang": lang})

return default_languages

def validate_recommendations__default_languages(self, default_languages):
return self._validate_languages(default_languages)

def validate_feed_foryou__languages(self, default_languages):
return self._validate_languages(default_languages)

def validate_feed_topitems__languages(self, default_languages):
return self._validate_languages(default_languages)


class TournesolUserSettingsSerializer(serializers.Serializer):
"""
Expand Down
90 changes: 51 additions & 39 deletions backend/core/tests/serializers/test_user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,57 +107,69 @@ class VideosPollUserSettingsSerializerTestCase(TestCase):
TestCase of the `VideosPollUserSettingsSerializer` serializer.
"""

def test_validate_recommendations__default_languages(self):
def test_validate_feed__languages(self):
"""
The `validate_recommendations__default_languages` setting must raise
The `validate_feed_foryou__languages` setting and similar must raise
an error for unknown languages.
"""

serializer = VideosPollUserSettingsSerializer(
data={"recommendations__default_languages": []}
)
self.assertEqual(serializer.is_valid(), True)
# Also test the deprecated `recommendations__default_languages`
# setting, until its removal.
for setting in [
"feed_foryou__languages",
"feed_topitems__languages",
"recommendations__default_languages"
]:

serializer = VideosPollUserSettingsSerializer(
data={"recommendations__default_languages": ["fr"]}
)
self.assertEqual(serializer.is_valid(), True)
serializer = VideosPollUserSettingsSerializer(
data={setting: []}
)
self.assertEqual(serializer.is_valid(), True)

serializer = VideosPollUserSettingsSerializer(
data={"recommendations__default_languages": ["fr", "en"]}
)
self.assertEqual(serializer.is_valid(), True)
serializer = VideosPollUserSettingsSerializer(
data={setting: ["fr"]}
)
self.assertEqual(serializer.is_valid(), True)

serializer = VideosPollUserSettingsSerializer(
data={"recommendations__default_languages": ["not_a_language"]}
)
self.assertEqual(serializer.is_valid(), False)
self.assertIn("recommendations__default_languages", serializer.errors)
serializer = VideosPollUserSettingsSerializer(
data={setting: ["fr", "en"]}
)
self.assertEqual(serializer.is_valid(), True)

serializer = VideosPollUserSettingsSerializer(
data={"recommendations__default_languages": ["en", "not_a_language"]}
)
self.assertEqual(serializer.is_valid(), False)
self.assertIn("recommendations__default_languages", serializer.errors)
serializer = VideosPollUserSettingsSerializer(
data={setting: ["not_a_language"]}
)
self.assertEqual(serializer.is_valid(), False)
self.assertIn(setting, serializer.errors)

def test_validate_recommendations__default_date(self):
serializer = VideosPollUserSettingsSerializer(
data={setting: ["en", "not_a_language"]}
)
self.assertEqual(serializer.is_valid(), False)
self.assertIn(setting, serializer.errors)

def test_validate_feed_foryou__date(self):
"""
The `validate_recommendations__default_date` setting must accept only
The `validate_feed_foryou__date` setting must accept only
a specific set of date.
"""

for date in ["TODAY", "WEEK", "MONTH", "YEAR", "ALL_TIME"]:
serializer = VideosPollUserSettingsSerializer(
data={"recommendations__default_date": date}
)
self.assertEqual(serializer.is_valid(), True)
# Also test the deprecated `recommendations__default_date`
# setting, until its removal.
for setting in ["feed_foryou__date", "recommendations__default_date"]:

# A blank value means no default date.
serializer = VideosPollUserSettingsSerializer(data={"recommendations__default_date": ""})
self.assertEqual(serializer.is_valid(), True)
for date in ["TODAY", "WEEK", "MONTH", "YEAR", "ALL_TIME"]:
serializer = VideosPollUserSettingsSerializer(
data={setting: date}
)
self.assertEqual(serializer.is_valid(), True)

serializer = VideosPollUserSettingsSerializer(
data={"recommendations__default_date": ["not_a_valid_date"]}
)
self.assertEqual(serializer.is_valid(), False)
self.assertIn("recommendations__default_date", serializer.errors)
# A blank value means no default date.
serializer = VideosPollUserSettingsSerializer(data={setting: ""})
self.assertEqual(serializer.is_valid(), True)

serializer = VideosPollUserSettingsSerializer(
data={setting: ["not_a_valid_date"]}
)
self.assertEqual(serializer.is_valid(), False)
self.assertIn(setting, serializer.errors)
16 changes: 9 additions & 7 deletions backend/core/tests/test_api_user_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ def setUp(self):
"videos": {
"comparison__criteria_order": ["reliability"],
"rate_later__auto_remove": 16,
"recommendation__default_languages": ["en"],
"recommendation__default_date": "Week",
"recommendation__default_unsafe": False,
"feed_foryou__languages": ["en"],
"feed_foryou__date": "Week",
"feed_foryou__unsafe": False,
"feed_topitems__languages": ["en", "fr"],
}
}

Expand Down Expand Up @@ -52,10 +53,11 @@ def test_auth_200_get(self):
"comparison_ui__weekly_collective_goal_mobile": True,
"extension__search_reco": True,
"rate_later__auto_remove": 99,
"recommendations__default_languages": ["en"],
"recommendations__default_date": "WEEK",
"recommendations__default_exclude_compared_entities": False,
"recommendations__default_unsafe": False,
"feed_foryou__languages": ["en"],
"feed_foryou__date": "WEEK",
"feed_foryou__exclude_compared_entities": False,
"feed_foryou__unsafe": False,
"feed_topitems__languages": ["en", "fr"],
},
}
self.user.settings = new_settings
Expand Down
51 changes: 29 additions & 22 deletions backend/tournesol/tests/test_api_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,35 +454,42 @@ def test_anon_200_get_with_yt_connection_error(self):
class DynamicRecommendationsPreviewTestCase(TestCase):
def setUp(self):
self.client = APIClient()
self.preview_url = "/preview/recommendations"
self.preview_internal_url = "/preview/_recommendations"
self.preview_urls = [
"/preview/search",
"/preview/feed/top",
"/preview/recommendations",
]

def test_recommendations_preview_query_redirection(self):
response = self.client.get(f"{self.preview_url}/?language=fr&date=Month")
self.assertEqual(response.status_code, 302)
self.assertRegex(
response.headers["location"],
rf"{self.preview_internal_url}/\?metadata%5Blanguage%5D=fr&date_gte=.*",
)

def test_recommendations_preview_query_redirection_all_languages_filter(self):
response = self.client.get(f"{self.preview_url}/?language=")
self.assertEqual(response.status_code, 302)
# No filter should be present in the redirection
self.assertEqual(response.headers["location"], f"{self.preview_internal_url}/?")

def test_recommendations_preview_empty_fields(self):
response = self.client.get(f"{self.preview_url}/?duration_lte&duration_gte")
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["location"], f"{self.preview_internal_url}/?")

def test_recommendations_preview_internal_route(self):
def test_preview_internal_route(self):
response = self.client.get(f"{self.preview_internal_url}/?metadata[language]=fr")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers["Content-Type"], "image/jpeg")
self.assertNotIn("Content-Disposition", response.headers)

def test_recommendations_preview_without_tournesol_score(self):
def test_preview_redirection(self):
for url in self.preview_urls:
response = self.client.get(f"{url}/?language=fr&date=Month")
self.assertEqual(response.status_code, 302)
self.assertRegex(
response.headers["location"],
rf"{self.preview_internal_url}/\?metadata%5Blanguage%5D=fr&date_gte=.*",
)

def test_preview_redirection_with_all_languages(self):
for url in self.preview_urls:
response = self.client.get(f"{url}/?language=")
self.assertEqual(response.status_code, 302)
# No filter should be present in the redirection
self.assertEqual(response.headers["location"], f"{self.preview_internal_url}/?")

def test_preview_redirection_with_empty_parameters(self):
for url in self.preview_urls:
response = self.client.get(f"{url}/?duration_lte&duration_gte")
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["location"], f"{self.preview_internal_url}/?")

def test_preview_entities_without_tournesol_score(self):
"""
The API shouldn't fail when displaying entities without computed
Tournesol score.
Expand Down
20 changes: 15 additions & 5 deletions backend/tournesol/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
path(
"users/me/suggestions/<str:poll_name>/tocompare/",
SuggestionsToCompare.as_view(),
name="suggestions_me_to_compare"
name="suggestions_me_to_compare",
),
# Sub-samples API
path(
Expand Down Expand Up @@ -228,16 +228,26 @@
DynamicWebsitePreviewFAQ.as_view(),
name="website_preview_faq",
),
# This route show the preview for the recommendations page
# after preview/recommendations route rewrite the url paramaters
# to match backend parameters and redirect
# This route creates the preview of an entity list.
path(
"preview/_recommendations/",
DynamicWebsitePreviewRecommendations.as_view(),
name="website_preview_recommendations_internal",
),
# This route rewrite the url for the recommendations page preview
# These routes rewrite the URL parameters to match those used by the
# recommendations of the polls API.
re_path(
r"^preview/search/?$",
get_preview_recommendations_redirect_params,
name="website_preview_search_redirect",
),
re_path(
r"^preview/feed/top/?$",
get_preview_recommendations_redirect_params,
name="website_preview_feed_topitems_redirect",
),
re_path(
# kept for backward compatibility, replaced by preview/search/
r"^preview/recommendations/?$",
get_preview_recommendations_redirect_params,
name="website_preview_recommendations_redirect",
Expand Down
4 changes: 3 additions & 1 deletion backend/tournesol/views/previews/recommendations.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
def get_preview_recommendations_redirect_params(request):
"""
Preview of a Recommendations page.
Returns HTTP redirection to transform the query parameters into the format used by the backend.
Returns a HTTP redirection to format the query parameters to match those
used by the polls API.
"""
# pylint: disable=too-many-branches
params = request.GET
Expand Down
2 changes: 1 addition & 1 deletion browser-extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tournesol-extension",
"version": "3.6.1",
"version": "3.7.0",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
Expand Down
5 changes: 1 addition & 4 deletions browser-extension/prepareExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,7 @@ const manifest = {
production: ['https://tournesol.app/*'],
'dev-env': ['http://localhost:3000/*'],
}),
js: [
'fetchTournesolToken.js',
'fetchTournesolRecommendationsLanguages.js',
],
js: ['fetchTournesolToken.js'],
run_at: 'document_end',
all_frames: true,
},
Expand Down
Loading

0 comments on commit fa3fb06

Please sign in to comment.