From 93b0c4e9fc136efbb7174b9cd687d3b2380db214 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 12 Dec 2023 18:09:30 +0100 Subject: [PATCH 01/13] [ext] Allow the extension to connect to dev-env --- .../workflows/browser-extension-release.yml | 4 + browser-extension/.eslintignore | 1 + browser-extension/.gitignore | 3 + browser-extension/README.md | 10 ++ browser-extension/build.sh | 3 +- browser-extension/package.json | 1 + browser-extension/prepareExtension.js | 119 ++++++++++++++++++ browser-extension/prepareTools.js | 44 +++++++ browser-extension/src/addModal.js | 4 +- browser-extension/src/addRateButtons.js | 4 +- browser-extension/src/addVideoStatistics.js | 6 +- browser-extension/src/background.js | 4 +- browser-extension/src/browserAction/menu.js | 7 +- browser-extension/src/manifest.json | 69 ---------- .../tournesolContainer/TournesolContainer.js | 10 +- browser-extension/src/utils.js | 6 +- 16 files changed, 207 insertions(+), 88 deletions(-) create mode 100644 browser-extension/.eslintignore create mode 100644 browser-extension/prepareExtension.js create mode 100644 browser-extension/prepareTools.js delete mode 100644 browser-extension/src/manifest.json diff --git a/.github/workflows/browser-extension-release.yml b/.github/workflows/browser-extension-release.yml index c6a6686b01..12946bd181 100644 --- a/.github/workflows/browser-extension-release.yml +++ b/.github/workflows/browser-extension-release.yml @@ -19,9 +19,13 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 0 + - uses: actions/setup-node@v2 + with: + node-version: '18' - name: Check extension version in the manifest run: | + node prepareExtension.js ext_version=$(python -c 'import json; print(json.load(open("src/manifest.json"))["version"])') tag_exist=$(git tag -l "browser-extension-v$ext_version" | wc -l) echo "ext_version=$ext_version" >> $GITHUB_ENV diff --git a/browser-extension/.eslintignore b/browser-extension/.eslintignore new file mode 100644 index 0000000000..9193f811f9 --- /dev/null +++ b/browser-extension/.eslintignore @@ -0,0 +1 @@ +src/config.js diff --git a/browser-extension/.gitignore b/browser-extension/.gitignore index cceca162b9..b57f663a88 100644 --- a/browser-extension/.gitignore +++ b/browser-extension/.gitignore @@ -1,2 +1,5 @@ *.zip node_modules/ +src/manifest.json +src/config.js +src/importWrappers/ diff --git a/browser-extension/README.md b/browser-extension/README.md index 9b75280fdd..0ef6b248e3 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -40,6 +40,16 @@ available here: yarn install ``` +### Prepare the extension + +Before loading the extension into your browser, you need to run `node prepareExtension.js`. It will generate `manifest.json`, `config.js` and the import wrappers (small scripts that allow us to use ECMAScript modules in content scripts). + +By default, `prepareExtension.js` creates an extension that connects to the production Tournesol website. If you want to connect to your development servers instead, you can specify a `TOURNESOL_ENV` environment variable. For example: + +``` +TOURNESOL_ENV=dev-env node prepareExtension.js +``` + ### Code Quality We use `ESLint` to find and fix problems in the JavaScript code. diff --git a/browser-extension/build.sh b/browser-extension/build.sh index e1a2e44fe2..d723ea5e2c 100755 --- a/browser-extension/build.sh +++ b/browser-extension/build.sh @@ -15,6 +15,8 @@ TARGET_BASENAME='tournesol_extension.zip' pushd ${SCRIPT_PATH} > /dev/null +node prepareExtension.js + # zip the sources pushd ${SOURCE_DIR} > /dev/null zip -r -FS ../${TARGET_BASENAME} * @@ -23,4 +25,3 @@ popd > /dev/null # zip the license zip ${TARGET_BASENAME} LICENSE popd > /dev/null - diff --git a/browser-extension/package.json b/browser-extension/package.json index 2e9b6bd199..e8db63021a 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,6 +1,7 @@ { "name": "tournesol-extension", "license": "AGPL-3.0-or-later", + "type": "module", "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix ." diff --git a/browser-extension/prepareExtension.js b/browser-extension/prepareExtension.js new file mode 100644 index 0000000000..833b34ed8d --- /dev/null +++ b/browser-extension/prepareExtension.js @@ -0,0 +1,119 @@ +/* eslint-env node */ + +import { + getForEnv, + generateImportWrappers, + writeManifest, + writeConfig, +} from './prepareTools.js'; + +const env = process.env.TOURNESOL_ENV || 'production'; + +const manifest = { + name: 'Tournesol Extension', + version: '3.4.0', + description: 'Open Tournesol directly from YouTube', + permissions: [ + ...getForEnv( + { + production: ['https://tournesol.app/', 'https://api.tournesol.app/'], + 'dev-env': [ + 'http://localhost/', + 'http://localhost:3000/', + 'http://localhost:8000/', + ], + }, + env + ), + 'https://www.youtube.com/', + 'activeTab', + 'contextMenus', + 'storage', + 'webNavigation', + 'webRequest', + 'webRequestBlocking', + ], + manifest_version: 2, + icons: { + 64: 'Logo64.png', + 128: 'Logo128.png', + 512: 'Logo512.png', + }, + background: { + page: 'background.html', + persistent: true, + }, + browser_action: { + default_icon: { + 16: 'Logo16.png', + 64: 'Logo64.png', + }, + default_title: 'Tournesol actions', + default_popup: 'browserAction/menu.html', + }, + content_scripts: [ + { + matches: ['https://*.youtube.com/*'], + js: ['displayHomeRecommendations.js', 'displaySearchRecommendations.js'], + css: ['addTournesolRecommendations.css'], + run_at: 'document_start', + all_frames: true, + }, + { + matches: ['https://*.youtube.com/*'], + js: ['addVideoStatistics.js', 'addModal.js', 'addRateButtons.js'], + css: ['addVideoStatistics.css', 'addModal.css', 'addRateButtons.css'], + run_at: 'document_end', + all_frames: true, + }, + { + matches: getForEnv( + { + production: ['https://tournesol.app/*'], + 'dev-env': ['http://localhost:3000/*'], + }, + env + ), + js: [ + 'fetchTournesolToken.js', + 'fetchTournesolRecommendationsLanguages.js', + ], + run_at: 'document_end', + all_frames: true, + }, + ], + options_ui: { + page: 'options/options.html', + open_in_tab: true, + }, + default_locale: 'en', + web_accessible_resources: [ + 'Logo128.png', + 'html/*', + 'images/*', + 'utils.js', + 'models/*', + ], +}; + +const config = getForEnv( + { + production: { + frontendUrl: 'https://tournesol.app/', + frontendHostEquals: 'tournesol.app', + apiUrl: 'https://api.tournesol.app/', + }, + 'dev-env': { + frontendUrl: 'http://localhost:3000/', + frontendHostEquals: 'localhost:3000', + apiUrl: 'http://localhost:8000/', + }, + }, + env +); + +(async () => { + await generateImportWrappers(manifest); + await writeManifest(manifest, 'src/manifest.json'); + await writeConfig(config, 'src/config.js'); +})(); diff --git a/browser-extension/prepareTools.js b/browser-extension/prepareTools.js new file mode 100644 index 0000000000..34007fabb2 --- /dev/null +++ b/browser-extension/prepareTools.js @@ -0,0 +1,44 @@ +/* eslint-env node */ + +import { writeFile, mkdir } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +export const getForEnv = (object, env) => { + const result = object[env]; + if (result === undefined) { + throw new Error( + `No value found for the environment ${JSON.stringify(env)}` + ); + } + return result; +}; + +export const generateImportWrappers = async (manifest) => { + await Promise.all( + manifest['content_scripts'].map(async (contentScript) => { + await Promise.all( + contentScript.js.map(async (js, i) => { + const content = `import(chrome.runtime.getURL('../${js}'));\n`; + const newJs = join('importWrappers', js); + const path = join('src', newJs); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, content); + contentScript.js[i] = newJs; + }) + ); + }) + ); +}; + +export const writeManifest = async (manifest, outputPath) => { + const content = JSON.stringify(manifest, null, 2); + await writeFile(outputPath, content); +}; + +export const writeConfig = async (config, outputPath) => { + let content = ''; + for (let [key, value] of Object.entries(config)) { + content += `export const ${key} = ${JSON.stringify(value)};\n`; + } + await writeFile(outputPath, content); +}; diff --git a/browser-extension/src/addModal.js b/browser-extension/src/addModal.js index 0d85bd7df6..756ebc0b76 100644 --- a/browser-extension/src/addModal.js +++ b/browser-extension/src/addModal.js @@ -5,6 +5,8 @@ * This content script is meant to be run on each YouTube page. */ +import { frontendUrl } from './config.js'; + // unique HTML id of the extension modal const EXT_MODAL_ID = 'x-tournesol-modal'; // the value of the CSS property display used to make the modal visible @@ -15,7 +17,7 @@ const EXT_MODAL_INVISIBLE_STATE = 'none'; // unique HTML id of the Tournesol iframe const IFRAME_TOURNESOL_ID = 'x-tournesol-iframe'; // URL of the Tournesol login page -const IFRAME_TOURNESOL_LOGIN_URL = 'https://tournesol.app/login?embed=1&dnt=1'; +const IFRAME_TOURNESOL_LOGIN_URL = `${frontendUrl}login?embed=1&dnt=1`; /** * YouTube doesnt completely load a page, so content script doesn't diff --git a/browser-extension/src/addRateButtons.js b/browser-extension/src/addRateButtons.js index c45ed99858..0f15da08b6 100644 --- a/browser-extension/src/addRateButtons.js +++ b/browser-extension/src/addRateButtons.js @@ -4,6 +4,8 @@ * This content script is meant to be run on each YouTube video page. */ +import { frontendUrl } from './config.js'; + const TS_ACTIONS_ROW_ID = 'ts-video-actions-row'; const TS_ACTIONS_ROW_BEFORE_REF = 'bottom-row'; @@ -114,7 +116,7 @@ function addRateButtons() { chrome.runtime.sendMessage({ message: 'displayModal', modalOptions: { - src: `https://tournesol.app/comparison?embed=1&utm_source=extension&utm_medium=frame&uidA=yt%3A${videoId}`, + src: `${frontendUrl}comparison?embed=1&utm_source=extension&utm_medium=frame&uidA=yt%3A${videoId}`, height: '90vh', }, }); diff --git a/browser-extension/src/addVideoStatistics.js b/browser-extension/src/addVideoStatistics.js index ffe0e11541..9ce07a2ca3 100644 --- a/browser-extension/src/addVideoStatistics.js +++ b/browser-extension/src/addVideoStatistics.js @@ -3,6 +3,8 @@ // This part is called on connection for the first time on youtube.com/* /* ********************************************************************* */ +import { frontendUrl } from './config.js'; + var browser = browser || chrome; document.addEventListener('yt-navigate-finish', process); @@ -87,9 +89,7 @@ function process() { // On click statisticsButton.onclick = () => { - open( - `https://tournesol.app/entities/yt:${videoId}?utm_source=extension` - ); + open(`${frontendUrl}entities/yt:${videoId}?utm_source=extension`); }; var div = diff --git a/browser-extension/src/background.js b/browser-extension/src/background.js index a2fac18649..74f646f9fd 100644 --- a/browser-extension/src/background.js +++ b/browser-extension/src/background.js @@ -10,6 +10,8 @@ import { getSingleSetting, } from './utils.js'; +import { frontendHostEquals } from './config.js'; + const oversamplingRatioForRecentVideos = 3; const oversamplingRatioForOldVideos = 50; // Higher means videos recommended can come from further down the recommandation list @@ -363,6 +365,6 @@ chrome.webNavigation.onHistoryStateUpdated.addListener( chrome.tabs.sendMessage(event.tabId, 'historyStateUpdated'); }, { - url: [{ hostEquals: 'tournesol.app' }], + url: [{ hostEquals: frontendHostEquals }], } ); diff --git a/browser-extension/src/browserAction/menu.js b/browser-extension/src/browserAction/menu.js index 9734f50b17..28fe8456bc 100644 --- a/browser-extension/src/browserAction/menu.js +++ b/browser-extension/src/browserAction/menu.js @@ -1,4 +1,5 @@ import { addRateLater } from '../utils.js'; +import { frontendUrl } from '../config.js'; const i18n = chrome.i18n; @@ -24,7 +25,7 @@ function get_current_tab_video_id() { */ function openTournesolHome() { chrome.tabs.create({ - url: 'https://tournesol.app?utm_source=extension&utm_medium=menu', + url: `${frontendUrl}?utm_source=extension&utm_medium=menu`, }); } @@ -36,7 +37,7 @@ function rateNowAction(event) { get_current_tab_video_id().then( (videoId) => { chrome.tabs.create({ - url: `https://tournesol.app/comparison?uidA=yt:${videoId}&utm_source=extension&utm_medium=menu`, + url: `${frontendUrl}comparison?uidA=yt:${videoId}&utm_source=extension&utm_medium=menu`, }); }, () => { @@ -93,7 +94,7 @@ function openAnalysisPageAction(event) { get_current_tab_video_id().then( (videoId) => { chrome.tabs.create({ - url: `https://tournesol.app/entities/yt:${videoId}?utm_source=extension&utm_medium=menu`, + url: `${frontendUrl}entities/yt:${videoId}?utm_source=extension&utm_medium=menu`, }); }, () => { diff --git a/browser-extension/src/manifest.json b/browser-extension/src/manifest.json deleted file mode 100644 index ac59677e17..0000000000 --- a/browser-extension/src/manifest.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "Tournesol Extension", - "version": "3.4.0", - "description": "Open Tournesol directly from YouTube", - "permissions": [ - "https://tournesol.app/", - "https://api.tournesol.app/", - "https://www.youtube.com/", - "activeTab", - "contextMenus", - "storage", - "webNavigation", - "webRequest", - "webRequestBlocking" - ], - "manifest_version": 2, - "icons": { - "64": "Logo64.png", - "128": "Logo128.png", - "512": "Logo512.png" - }, - "background": { - "page": "background.html", - "persistent": true - }, - "browser_action": { - "default_icon": { - "16": "Logo16.png", - "64": "Logo64.png" - }, - "default_title": "Tournesol actions", - "default_popup": "browserAction/menu.html" - }, - "content_scripts": [ - { - "matches": ["https://*.youtube.com/*"], - "js": ["displayHomeRecommendations.js","displaySearchRecommendations.js"], - "css": ["addTournesolRecommendations.css"], - "run_at": "document_start", - "all_frames": true - }, - { - "matches": ["https://*.youtube.com/*"], - "js": ["addVideoStatistics.js", "addModal.js", "addRateButtons.js"], - "css": [ - "addVideoStatistics.css", - "addModal.css", - "addRateButtons.css" - ], - "run_at": "document_end", - "all_frames": true - }, - { - "matches": ["https://tournesol.app/*"], - "js": [ - "fetchTournesolToken.js", - "fetchTournesolRecommendationsLanguages.js" - ], - "run_at": "document_end", - "all_frames": true - } - ], - "options_ui": { - "page": "options/options.html", - "open_in_tab": true - }, - "default_locale": "en", - "web_accessible_resources": ["Logo128.png", "html/*", "images/*", "utils.js", "models/*" ] -} diff --git a/browser-extension/src/models/tournesolContainer/TournesolContainer.js b/browser-extension/src/models/tournesolContainer/TournesolContainer.js index 19e5652811..5e170155e3 100644 --- a/browser-extension/src/models/tournesolContainer/TournesolContainer.js +++ b/browser-extension/src/models/tournesolContainer/TournesolContainer.js @@ -1,4 +1,5 @@ import { TournesolVideoCard } from '../tournesolVideoCard/TournesolVideoCard.js'; +import { frontendUrl } from '../../config.js'; export class TournesolContainer { /** @@ -70,7 +71,7 @@ export class TournesolContainer { 'tournesol_mui_like_button view_more_link small'; view_more_link.target = '_blank'; view_more_link.rel = 'noopener'; - view_more_link.href = `https://tournesol.app/recommendations?search=${ + view_more_link.href = `${frontendUrl}recommendations?search=${ this.recommendations.searchQuery }&language=${this.recommendations.recommandationsLanguages.replaceAll( ',', @@ -92,10 +93,7 @@ export class TournesolContainer { // Tournesol icon const tournesolIcon = document.createElement('img'); tournesolIcon.setAttribute('id', 'tournesol_icon'); - tournesolIcon.setAttribute( - 'src', - 'https://tournesol.app/svg/tournesol.svg' - ); + tournesolIcon.setAttribute('src', `${frontendUrl}svg/tournesol.svg`); tournesolIcon.setAttribute('width', '24'); topActionBar.append(tournesolIcon); @@ -108,7 +106,7 @@ export class TournesolContainer { // Learn more const learnMore = document.createElement('a'); learnMore.id = 'tournesol_link'; - learnMore.href = 'https://tournesol.app?utm_source=extension'; + learnMore.href = `${frontendUrl}utm_source=extension`; learnMore.target = '_blank'; learnMore.rel = 'noopener'; learnMore.append(chrome.i18n.getMessage('learnMore')); diff --git a/browser-extension/src/utils.js b/browser-extension/src/utils.js index e1bd5a8a1b..eaee2f8311 100644 --- a/browser-extension/src/utils.js +++ b/browser-extension/src/utils.js @@ -1,3 +1,5 @@ +import { apiUrl } from './config.js'; + export const getAccessToken = async () => { return new Promise((resolve) => { chrome.storage.local.get(['access_token'], (items) => { @@ -43,9 +45,7 @@ export const fetchTournesolApi = async (path, options = {}) => { } } - return fetch(`https://api.tournesol.app/${path}`, fetchOptions).catch( - console.error - ); + return fetch(`${apiUrl}${path}`, fetchOptions).catch(console.error); }; export const addRateLater = async (video_id) => { From df3ddd6680aa97db0e5d1c35f4a8eac559c7ce8f Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 19 Dec 2023 16:08:17 +0100 Subject: [PATCH 02/13] [ext] Fix blocked imports on chrome and e2e --- .github/workflows/e2e.yml | 5 +++++ browser-extension/prepareExtension.js | 1 + browser-extension/prepareTools.js | 1 + 3 files changed, 7 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 680abd9d68..7c43e0df48 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -18,6 +18,11 @@ jobs: with: node-version: '18' + - name: Prepare extension + working-directory: browser-extension + run: | + node prepareExtension.js + - uses: cypress-io/github-action@v5 with: working-directory: tests diff --git a/browser-extension/prepareExtension.js b/browser-extension/prepareExtension.js index 833b34ed8d..f4e1ea1ff1 100644 --- a/browser-extension/prepareExtension.js +++ b/browser-extension/prepareExtension.js @@ -93,6 +93,7 @@ const manifest = { 'images/*', 'utils.js', 'models/*', + 'config.js', ], }; diff --git a/browser-extension/prepareTools.js b/browser-extension/prepareTools.js index 34007fabb2..a33c7ad858 100644 --- a/browser-extension/prepareTools.js +++ b/browser-extension/prepareTools.js @@ -24,6 +24,7 @@ export const generateImportWrappers = async (manifest) => { await mkdir(dirname(path), { recursive: true }); await writeFile(path, content); contentScript.js[i] = newJs; + manifest['web_accessible_resources'].push(js); }) ); }) From 928ad8d82deaba547b188184128c6a584b46298e Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 2 Jan 2024 15:28:47 +0100 Subject: [PATCH 03/13] [ext] Stop adding trailing slash to home url And also fix the missing `?` in `learnMore.href`. --- browser-extension/prepareExtension.js | 10 ++++++---- browser-extension/src/addModal.js | 2 +- browser-extension/src/addRateButtons.js | 2 +- browser-extension/src/addVideoStatistics.js | 2 +- browser-extension/src/browserAction/menu.js | 4 ++-- .../models/tournesolContainer/TournesolContainer.js | 6 +++--- browser-extension/src/utils.js | 2 +- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/browser-extension/prepareExtension.js b/browser-extension/prepareExtension.js index f4e1ea1ff1..ce30b735a4 100644 --- a/browser-extension/prepareExtension.js +++ b/browser-extension/prepareExtension.js @@ -97,17 +97,19 @@ const manifest = { ], }; +// Please DO NOT add a trailing slash to front end URL, this prevents +// creating duplicates in our web analytics tool const config = getForEnv( { production: { - frontendUrl: 'https://tournesol.app/', + frontendUrl: 'https://tournesol.app', frontendHostEquals: 'tournesol.app', - apiUrl: 'https://api.tournesol.app/', + apiUrl: 'https://api.tournesol.app', }, 'dev-env': { - frontendUrl: 'http://localhost:3000/', + frontendUrl: 'http://localhost:3000', frontendHostEquals: 'localhost:3000', - apiUrl: 'http://localhost:8000/', + apiUrl: 'http://localhost:8000', }, }, env diff --git a/browser-extension/src/addModal.js b/browser-extension/src/addModal.js index 756ebc0b76..4e73b44512 100644 --- a/browser-extension/src/addModal.js +++ b/browser-extension/src/addModal.js @@ -17,7 +17,7 @@ const EXT_MODAL_INVISIBLE_STATE = 'none'; // unique HTML id of the Tournesol iframe const IFRAME_TOURNESOL_ID = 'x-tournesol-iframe'; // URL of the Tournesol login page -const IFRAME_TOURNESOL_LOGIN_URL = `${frontendUrl}login?embed=1&dnt=1`; +const IFRAME_TOURNESOL_LOGIN_URL = `${frontendUrl}/login?embed=1&dnt=1`; /** * YouTube doesnt completely load a page, so content script doesn't diff --git a/browser-extension/src/addRateButtons.js b/browser-extension/src/addRateButtons.js index 0f15da08b6..15471db8b9 100644 --- a/browser-extension/src/addRateButtons.js +++ b/browser-extension/src/addRateButtons.js @@ -116,7 +116,7 @@ function addRateButtons() { chrome.runtime.sendMessage({ message: 'displayModal', modalOptions: { - src: `${frontendUrl}comparison?embed=1&utm_source=extension&utm_medium=frame&uidA=yt%3A${videoId}`, + src: `${frontendUrl}/comparison?embed=1&utm_source=extension&utm_medium=frame&uidA=yt%3A${videoId}`, height: '90vh', }, }); diff --git a/browser-extension/src/addVideoStatistics.js b/browser-extension/src/addVideoStatistics.js index 9ce07a2ca3..5e79a1fe99 100644 --- a/browser-extension/src/addVideoStatistics.js +++ b/browser-extension/src/addVideoStatistics.js @@ -89,7 +89,7 @@ function process() { // On click statisticsButton.onclick = () => { - open(`${frontendUrl}entities/yt:${videoId}?utm_source=extension`); + open(`${frontendUrl}/entities/yt:${videoId}?utm_source=extension`); }; var div = diff --git a/browser-extension/src/browserAction/menu.js b/browser-extension/src/browserAction/menu.js index 28fe8456bc..b6e5116826 100644 --- a/browser-extension/src/browserAction/menu.js +++ b/browser-extension/src/browserAction/menu.js @@ -37,7 +37,7 @@ function rateNowAction(event) { get_current_tab_video_id().then( (videoId) => { chrome.tabs.create({ - url: `${frontendUrl}comparison?uidA=yt:${videoId}&utm_source=extension&utm_medium=menu`, + url: `${frontendUrl}/comparison?uidA=yt:${videoId}&utm_source=extension&utm_medium=menu`, }); }, () => { @@ -94,7 +94,7 @@ function openAnalysisPageAction(event) { get_current_tab_video_id().then( (videoId) => { chrome.tabs.create({ - url: `${frontendUrl}entities/yt:${videoId}?utm_source=extension&utm_medium=menu`, + url: `${frontendUrl}/entities/yt:${videoId}?utm_source=extension&utm_medium=menu`, }); }, () => { diff --git a/browser-extension/src/models/tournesolContainer/TournesolContainer.js b/browser-extension/src/models/tournesolContainer/TournesolContainer.js index 5e170155e3..f2d76f3021 100644 --- a/browser-extension/src/models/tournesolContainer/TournesolContainer.js +++ b/browser-extension/src/models/tournesolContainer/TournesolContainer.js @@ -71,7 +71,7 @@ export class TournesolContainer { 'tournesol_mui_like_button view_more_link small'; view_more_link.target = '_blank'; view_more_link.rel = 'noopener'; - view_more_link.href = `${frontendUrl}recommendations?search=${ + view_more_link.href = `${frontendUrl}/recommendations?search=${ this.recommendations.searchQuery }&language=${this.recommendations.recommandationsLanguages.replaceAll( ',', @@ -93,7 +93,7 @@ export class TournesolContainer { // Tournesol icon const tournesolIcon = document.createElement('img'); tournesolIcon.setAttribute('id', 'tournesol_icon'); - tournesolIcon.setAttribute('src', `${frontendUrl}svg/tournesol.svg`); + tournesolIcon.setAttribute('src', `${frontendUrl}/svg/tournesol.svg`); tournesolIcon.setAttribute('width', '24'); topActionBar.append(tournesolIcon); @@ -106,7 +106,7 @@ export class TournesolContainer { // Learn more const learnMore = document.createElement('a'); learnMore.id = 'tournesol_link'; - learnMore.href = `${frontendUrl}utm_source=extension`; + learnMore.href = `${frontendUrl}?utm_source=extension`; learnMore.target = '_blank'; learnMore.rel = 'noopener'; learnMore.append(chrome.i18n.getMessage('learnMore')); diff --git a/browser-extension/src/utils.js b/browser-extension/src/utils.js index eaee2f8311..94056d0a77 100644 --- a/browser-extension/src/utils.js +++ b/browser-extension/src/utils.js @@ -45,7 +45,7 @@ export const fetchTournesolApi = async (path, options = {}) => { } } - return fetch(`${apiUrl}${path}`, fetchOptions).catch(console.error); + return fetch(`${apiUrl}/${path}`, fetchOptions).catch(console.error); }; export const addRateLater = async (video_id) => { From 7746b4bf4c4ad1917709c9f11e335b406b217dba Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 2 Jan 2024 15:31:05 +0100 Subject: [PATCH 04/13] [ext] Improve frontendHostEquals config name --- browser-extension/prepareExtension.js | 4 ++-- browser-extension/src/background.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/browser-extension/prepareExtension.js b/browser-extension/prepareExtension.js index ce30b735a4..e6bfd666bc 100644 --- a/browser-extension/prepareExtension.js +++ b/browser-extension/prepareExtension.js @@ -103,12 +103,12 @@ const config = getForEnv( { production: { frontendUrl: 'https://tournesol.app', - frontendHostEquals: 'tournesol.app', + frontendHost: 'tournesol.app', apiUrl: 'https://api.tournesol.app', }, 'dev-env': { frontendUrl: 'http://localhost:3000', - frontendHostEquals: 'localhost:3000', + frontendHost: 'localhost:3000', apiUrl: 'http://localhost:8000', }, }, diff --git a/browser-extension/src/background.js b/browser-extension/src/background.js index 74f646f9fd..7c56920775 100644 --- a/browser-extension/src/background.js +++ b/browser-extension/src/background.js @@ -10,7 +10,7 @@ import { getSingleSetting, } from './utils.js'; -import { frontendHostEquals } from './config.js'; +import { frontendHost } from './config.js'; const oversamplingRatioForRecentVideos = 3; const oversamplingRatioForOldVideos = 50; @@ -365,6 +365,6 @@ chrome.webNavigation.onHistoryStateUpdated.addListener( chrome.tabs.sendMessage(event.tabId, 'historyStateUpdated'); }, { - url: [{ hostEquals: frontendHostEquals }], + url: [{ hostEquals: frontendHost }], } ); From 0c5910ec64aba159d6108b94579ffc8766d18d55 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 2 Jan 2024 15:50:21 +0100 Subject: [PATCH 05/13] [ext] Move the extension version into package.json And use distinct .eslintrc.json for the scripts and for the extension code, to support top level await and remove the eslint-env comments in the scripts. The new src/.eslintrc.json is not added to the extension package because root hidden files are not added by build.sh. --- .../workflows/browser-extension-release.yml | 3 +-- browser-extension/.eslintrc.json | 12 ++++-------- browser-extension/package.json | 1 + browser-extension/prepareExtension.js | 7 ++++--- browser-extension/prepareTools.js | 9 ++++++--- browser-extension/src/.eslintrc.json | 19 +++++++++++++++++++ 6 files changed, 35 insertions(+), 16 deletions(-) create mode 100644 browser-extension/src/.eslintrc.json diff --git a/.github/workflows/browser-extension-release.yml b/.github/workflows/browser-extension-release.yml index 12946bd181..ed76050ab2 100644 --- a/.github/workflows/browser-extension-release.yml +++ b/.github/workflows/browser-extension-release.yml @@ -25,8 +25,7 @@ jobs: - name: Check extension version in the manifest run: | - node prepareExtension.js - ext_version=$(python -c 'import json; print(json.load(open("src/manifest.json"))["version"])') + ext_version=$(python -c 'import json; print(json.load(open("package.json"))["version"])') tag_exist=$(git tag -l "browser-extension-v$ext_version" | wc -l) echo "ext_version=$ext_version" >> $GITHUB_ENV echo "tag_exist=$tag_exist" >> $GITHUB_ENV diff --git a/browser-extension/.eslintrc.json b/browser-extension/.eslintrc.json index 9ba923de8a..01210790dc 100644 --- a/browser-extension/.eslintrc.json +++ b/browser-extension/.eslintrc.json @@ -1,15 +1,11 @@ { "env": { - "browser": true, - "es6": true, - "webextensions": true + "node": true, + "es6": true }, "parserOptions": { - "ecmaVersion": 11, - "sourceType": "module", - "ecmaFeatures": { - "modules": true - } + "ecmaVersion": 13, + "sourceType": "module" }, "plugins": [], "extends": ["eslint:recommended", "plugin:prettier/recommended"], diff --git a/browser-extension/package.json b/browser-extension/package.json index e8db63021a..53da5a78e2 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,5 +1,6 @@ { "name": "tournesol-extension", + "version": "3.4.0", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { diff --git a/browser-extension/prepareExtension.js b/browser-extension/prepareExtension.js index e6bfd666bc..cd54b2d711 100644 --- a/browser-extension/prepareExtension.js +++ b/browser-extension/prepareExtension.js @@ -1,17 +1,18 @@ -/* eslint-env node */ - import { getForEnv, generateImportWrappers, writeManifest, writeConfig, + readPackage, } from './prepareTools.js'; const env = process.env.TOURNESOL_ENV || 'production'; +const { version } = await readPackage(); + const manifest = { name: 'Tournesol Extension', - version: '3.4.0', + version, description: 'Open Tournesol directly from YouTube', permissions: [ ...getForEnv( diff --git a/browser-extension/prepareTools.js b/browser-extension/prepareTools.js index a33c7ad858..37cc214ec6 100644 --- a/browser-extension/prepareTools.js +++ b/browser-extension/prepareTools.js @@ -1,6 +1,4 @@ -/* eslint-env node */ - -import { writeFile, mkdir } from 'node:fs/promises'; +import { writeFile, mkdir, readFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; export const getForEnv = (object, env) => { @@ -43,3 +41,8 @@ export const writeConfig = async (config, outputPath) => { } await writeFile(outputPath, content); }; + +export const readPackage = async () => { + const packageContent = await readFile('package.json'); + return JSON.parse(packageContent); +}; diff --git a/browser-extension/src/.eslintrc.json b/browser-extension/src/.eslintrc.json new file mode 100644 index 0000000000..9ba923de8a --- /dev/null +++ b/browser-extension/src/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "es6": true, + "webextensions": true + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module", + "ecmaFeatures": { + "modules": true + } + }, + "plugins": [], + "extends": ["eslint:recommended", "plugin:prettier/recommended"], + "rules": { + "prettier/prettier": "error" + } +} From 55eda5377d93cd442dc05ee8c7bc80eb284d80bb Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 2 Jan 2024 15:42:46 +0100 Subject: [PATCH 06/13] [ext] Add scripts to prepare in package.json --- browser-extension/README.md | 8 ++------ browser-extension/package.json | 2 ++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/browser-extension/README.md b/browser-extension/README.md index 0ef6b248e3..e9b58ab40a 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -42,13 +42,9 @@ yarn install ### Prepare the extension -Before loading the extension into your browser, you need to run `node prepareExtension.js`. It will generate `manifest.json`, `config.js` and the import wrappers (small scripts that allow us to use ECMAScript modules in content scripts). +Before loading the extension into your browser, you need to run `yarn prepare`. It will generate `manifest.json`, `config.js` and the import wrappers (small scripts that allow us to use ECMAScript modules in content scripts). -By default, `prepareExtension.js` creates an extension that connects to the production Tournesol website. If you want to connect to your development servers instead, you can specify a `TOURNESOL_ENV` environment variable. For example: - -``` -TOURNESOL_ENV=dev-env node prepareExtension.js -``` +By default, the script creates an extension that connects to the production Tournesol website. If you want to connect to your development servers, you can run `yarn prepare:dev` instead. ### Code Quality diff --git a/browser-extension/package.json b/browser-extension/package.json index 53da5a78e2..1a3fa3f7df 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -4,6 +4,8 @@ "license": "AGPL-3.0-or-later", "type": "module", "scripts": { + "prepare": "node prepareExtension.js", + "prepare:dev": "TOURNESOL_ENV=dev-env node prepareExtension.js", "lint": "eslint .", "lint:fix": "eslint --fix ." }, From 21414cabda8d3eeb565ed173954fc2023e1fe61b Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Tue, 2 Jan 2024 16:19:15 +0100 Subject: [PATCH 07/13] [ext] Use config urls in options page --- browser-extension/src/options/options.html | 5 ++--- browser-extension/src/options/options.js | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/browser-extension/src/options/options.html b/browser-extension/src/options/options.html index 4eca6dee59..f5a0d0356b 100644 --- a/browser-extension/src/options/options.html +++ b/browser-extension/src/options/options.html @@ -24,7 +24,7 @@

Manage your preferences

These parameters are used only when you don't have a - Tournesol account, + Tournesol account, or when you are not logged in.

@@ -328,10 +328,9 @@
Languages
- + diff --git a/browser-extension/src/options/options.js b/browser-extension/src/options/options.js index 28818ff11c..8b154ff27f 100644 --- a/browser-extension/src/options/options.js +++ b/browser-extension/src/options/options.js @@ -7,6 +7,8 @@ * It also manage the loading and saving the local preferences. */ +import { frontendUrl } from '../config.js'; + const DEFAULT_RECO_LANG = ['en']; // This delay is designed to be few miliseconds slower than our CSS fadeOut // animation to let the success message disappear before re-enabling the @@ -266,3 +268,9 @@ document .addEventListener('submit', (event) => { event.preventDefault(); }); + +document.getElementById('account-link').href = `${frontendUrl}/signup`; + +document + .getElementById('iframe-tournesol-preferences') + .setAttribute('src', `${frontendUrl}/settings/preferences?embed=1`); From ce4cb5d208e65afb0029002f90873bbeb2d67869 Mon Sep 17 00:00:00 2001 From: Gresille & Siffle <39056254+GresilleSiffle@users.noreply.github.com> Date: Thu, 4 Jan 2024 10:42:29 +0100 Subject: [PATCH 08/13] [back][front] feat: display context alerts in /comparisons/ (#1865) Co-authored-by: Adrien Matissart --- backend/tournesol/models/poll.py | 9 +- backend/tournesol/serializers/comparison.py | 68 ++++- .../tournesol/tests/test_api_comparison.py | 259 +++++++++--------- backend/tournesol/views/comparison.py | 24 +- frontend/scripts/openapi.yaml | 24 ++ frontend/src/components/entity/EntityCard.tsx | 37 +-- .../features/comparisons/ComparisonList.tsx | 35 +-- 7 files changed, 267 insertions(+), 189 deletions(-) diff --git a/backend/tournesol/models/poll.py b/backend/tournesol/models/poll.py index 02004ce42a..c07a9bf3cd 100644 --- a/backend/tournesol/models/poll.py +++ b/backend/tournesol/models/poll.py @@ -140,15 +140,20 @@ def entity_has_unsafe_context(self, entity_metadata) -> tuple: return False, None - def get_entity_contexts(self, entity_metadata) -> list: + def get_entity_contexts(self, entity_metadata, prefetched_contexts=None) -> list: """ Return a list of all enabled contexts matching the given entity metadata. """ contexts = [] + if prefetched_contexts is not None: + available_contexts = prefetched_contexts + else: + available_contexts = self.all_entity_contexts.all() + # The entity contexts are expected to be already prefetched. - for entity_context in self.all_entity_contexts.all(): + for entity_context in available_contexts: if not entity_context.enabled: continue diff --git a/backend/tournesol/serializers/comparison.py b/backend/tournesol/serializers/comparison.py index 8ac5c5c43f..18e9f1ec04 100644 --- a/backend/tournesol/serializers/comparison.py +++ b/backend/tournesol/serializers/comparison.py @@ -5,6 +5,7 @@ from tournesol.models import Comparison, ComparisonCriteriaScore from tournesol.serializers.entity import RelatedEntitySerializer +from tournesol.serializers.entity_context import EntityContextSerializer class ComparisonCriteriaScoreSerializer(ModelSerializer): @@ -22,6 +23,11 @@ def validate_criteria(self, value): class ComparisonSerializerMixin: + def format_entity_contexts(self, poll, contexts, metadata): + return EntityContextSerializer( + poll.get_entity_contexts(metadata, contexts), many=True + ).data + def reverse_criteria_scores(self, criteria_scores): opposite_scores = criteria_scores.copy() for index, score in enumerate(criteria_scores): @@ -35,9 +41,7 @@ def validate_criteria_scores(self, value): score["criteria"] for score in value ) if missing_criterias: - raise ValidationError( - f"Missing required criteria: {','.join(missing_criterias)}" - ) + raise ValidationError(f"Missing required criteria: {','.join(missing_criterias)}") return value @@ -53,12 +57,23 @@ class ComparisonSerializer(ComparisonSerializerMixin, ModelSerializer): entity_a = RelatedEntitySerializer(source="entity_1") entity_b = RelatedEntitySerializer(source="entity_2") + entity_a_contexts = EntityContextSerializer(read_only=True, many=True, default=[]) + entity_b_contexts = EntityContextSerializer(read_only=True, many=True, default=[]) + criteria_scores = ComparisonCriteriaScoreSerializer(many=True) user = serializers.HiddenField(default=serializers.CurrentUserDefault()) class Meta: model = Comparison - fields = ["user", "entity_a", "entity_b", "criteria_scores", "duration_ms"] + fields = [ + "user", + "entity_a", + "entity_b", + "entity_a_contexts", + "entity_b_contexts", + "criteria_scores", + "duration_ms", + ] def to_representation(self, instance): """ @@ -69,8 +84,17 @@ def to_representation(self, instance): if self.context.get("reverse", False): ret["entity_a"], ret["entity_b"] = ret["entity_b"], ret["entity_a"] - ret["criteria_scores"] = self.reverse_criteria_scores( - ret["criteria_scores"] + ret["criteria_scores"] = self.reverse_criteria_scores(ret["criteria_scores"]) + + poll = self.context.get("poll") + ent_contexts = self.context.get("entity_contexts") + + if poll is not None: + ret["entity_a_contexts"] = self.format_entity_contexts( + poll, ent_contexts, ret["entity_a"]["metadata"] + ) + ret["entity_b_contexts"] = self.format_entity_contexts( + poll, ent_contexts, ret["entity_b"]["metadata"] ) return ret @@ -91,9 +115,7 @@ def create(self, validated_data): ) for criteria_score in criteria_scores: - ComparisonCriteriaScore.objects.create( - comparison=comparison, **criteria_score - ) + ComparisonCriteriaScore.objects.create(comparison=comparison, **criteria_score) return comparison @@ -111,10 +133,19 @@ class ComparisonUpdateSerializer(ComparisonSerializerMixin, ModelSerializer): criteria_scores = ComparisonCriteriaScoreSerializer(many=True) entity_a = RelatedEntitySerializer(source="entity_1", read_only=True) entity_b = RelatedEntitySerializer(source="entity_2", read_only=True) + entity_a_contexts = EntityContextSerializer(read_only=True, many=True, default=[]) + entity_b_contexts = EntityContextSerializer(read_only=True, many=True, default=[]) class Meta: model = Comparison - fields = ["criteria_scores", "duration_ms", "entity_a", "entity_b"] + fields = [ + "criteria_scores", + "duration_ms", + "entity_a", + "entity_b", + "entity_a_contexts", + "entity_b_contexts", + ] def to_representation(self, instance): """ @@ -128,8 +159,17 @@ def to_representation(self, instance): if self.context.get("reverse", False): ret["entity_a"], ret["entity_b"] = ret["entity_b"], ret["entity_a"] - ret["criteria_scores"] = self.reverse_criteria_scores( - ret["criteria_scores"] + ret["criteria_scores"] = self.reverse_criteria_scores(ret["criteria_scores"]) + + poll = self.context.get("poll") + ent_contexts = self.context.get("entity_contexts") + + if poll is not None: + ret["entity_a_contexts"] = self.format_entity_contexts( + poll, ent_contexts, ret["entity_a"]["metadata"] + ) + ret["entity_b_contexts"] = self.format_entity_contexts( + poll, ent_contexts, ret["entity_b"]["metadata"] ) ret.move_to_end("entity_b", last=False) @@ -144,9 +184,7 @@ def to_internal_value(self, data): ret = super().to_internal_value(data) if self.context.get("reverse", False): - ret["criteria_scores"] = self.reverse_criteria_scores( - ret["criteria_scores"] - ) + ret["criteria_scores"] = self.reverse_criteria_scores(ret["criteria_scores"]) return ret @transaction.atomic diff --git a/backend/tournesol/tests/test_api_comparison.py b/backend/tournesol/tests/test_api_comparison.py index 940bbae95f..9aa5dd12d3 100644 --- a/backend/tournesol/tests/test_api_comparison.py +++ b/backend/tournesol/tests/test_api_comparison.py @@ -20,6 +20,7 @@ Poll, RateLater, ) +from tournesol.models.entity_context import EntityContext, EntityContextLocale from tournesol.models.poll import ALGORITHM_MEHESTAN from tournesol.tests.factories.comparison import ComparisonCriteriaScoreFactory, ComparisonFactory from tournesol.tests.factories.entity import VideoFactory @@ -53,9 +54,7 @@ class ComparisonApiTestCase(TestCase): non_existing_comparison = { "entity_a": {"uid": _uid_01}, "entity_b": {"uid": _uid_03}, - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ], + "criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}], "duration_ms": 103, } @@ -66,9 +65,7 @@ def setUp(self): At least 4 videos and 2 users with 2 comparisons each are required. """ self.poll_videos = Poll.default_poll() - self.comparisons_base_url = "/users/me/comparisons/{}".format( - self.poll_videos.name - ) + self.comparisons_base_url = "/users/me/comparisons/{}".format(self.poll_videos.name) self.client = APIClient() @@ -116,6 +113,21 @@ def setUp(self): ), ] + self.ent_context_01 = EntityContext.objects.create( + name="context_safe", + origin=EntityContext.ASSOCIATION, + predicate={"video_id": self.videos[0].metadata["video_id"]}, + unsafe=False, + enabled=True, + poll=self.poll_videos, + ) + + self.ent_context_01_text = EntityContextLocale.objects.create( + context=self.ent_context_01, + language="en", + text="Hello context", + ) + def _remove_optional_fields(self, comparison): comparison.pop("duration_ms", None) @@ -245,28 +257,28 @@ def test_authenticated_can_create(self): self.assertEqual(comparison.duration_ms, data["duration_ms"]) comparison_criteria_scores = comparison.criteria_scores.all() - self.assertEqual( - comparison_criteria_scores.count(), len(data["criteria_scores"]) - ) + self.assertEqual(comparison_criteria_scores.count(), len(data["criteria_scores"])) self.assertEqual( comparison_criteria_scores[0].criteria, data["criteria_scores"][0]["criteria"], ) - self.assertEqual( - comparison_criteria_scores[0].score, data["criteria_scores"][0]["score"] - ) + self.assertEqual(comparison_criteria_scores[0].score, data["criteria_scores"][0]["score"]) self.assertEqual( comparison_criteria_scores[0].weight, data["criteria_scores"][0]["weight"] ) # check the representation integrity - self.assertEqual(response.data["entity_a"]["uid"], data["entity_a"]["uid"]) - self.assertEqual(response.data["entity_b"]["uid"], data["entity_b"]["uid"]) - self.assertEqual(response.data["duration_ms"], data["duration_ms"]) + resp_data = response.data + self.assertEqual(resp_data["entity_a"]["uid"], data["entity_a"]["uid"]) + self.assertEqual(resp_data["entity_b"]["uid"], data["entity_b"]["uid"]) - self.assertEqual( - len(response.data["criteria_scores"]), len(data["criteria_scores"]) - ) + self.assertEqual(len(resp_data["entity_a_contexts"]), 1) + self.assertEqual(resp_data["entity_a_contexts"][0]["text"], self.ent_context_01_text.text) + self.assertEqual(resp_data["entity_b_contexts"], []) + + self.assertEqual(resp_data["duration_ms"], data["duration_ms"]) + + self.assertEqual(len(response.data["criteria_scores"]), len(data["criteria_scores"])) self.assertEqual( response.data["criteria_scores"][0]["criteria"], data["criteria_scores"][0]["criteria"], @@ -376,9 +388,7 @@ def test_authenticated_can_create_without_optional(self): ) comparison_criteria_scores = comparison.criteria_scores.all() - self.assertEqual( - comparison_criteria_scores.count(), len(data["criteria_scores"]) - ) + self.assertEqual(comparison_criteria_scores.count(), len(data["criteria_scores"])) self.assertEqual(comparison_criteria_scores[0].weight, 1) @override_settings(YOUTUBE_API_KEY=None) @@ -599,24 +609,21 @@ def test_authenticated_can_list(self): self.assertEqual(len(response.data["results"]), comparisons_made.count()) # the comparisons must be ordered by datetime_lastedit - comparison1 = response.data["results"][0] - comparison2 = response.data["results"][1] + results = response.data["results"] + comp1 = results[0] + comp2 = results[1] - self.assertEqual( - comparison1["entity_a"]["uid"], self.comparisons[1].entity_1.uid - ) - self.assertEqual( - comparison1["entity_b"]["uid"], self.comparisons[1].entity_2.uid - ) - self.assertEqual(comparison1["duration_ms"], self.comparisons[1].duration_ms) + self.assertEqual(comp1["entity_a"]["uid"], self.comparisons[1].entity_1.uid) + self.assertEqual(comp1["entity_b"]["uid"], self.comparisons[1].entity_2.uid) + self.assertEqual(comp1["duration_ms"], self.comparisons[1].duration_ms) - self.assertEqual( - comparison2["entity_a"]["uid"], self.comparisons[0].entity_1.uid - ) - self.assertEqual( - comparison2["entity_b"]["uid"], self.comparisons[0].entity_2.uid - ) - self.assertEqual(comparison2["duration_ms"], self.comparisons[0].duration_ms) + self.assertEqual(comp2["entity_a"]["uid"], self.comparisons[0].entity_1.uid) + self.assertEqual(comp2["entity_b"]["uid"], self.comparisons[0].entity_2.uid) + + self.assertEqual(len(comp1["entity_a_contexts"]), 1) + self.assertEqual(comp1["entity_a_contexts"][0]["text"], self.ent_context_01_text.text) + self.assertEqual(comp1["entity_b_contexts"], []) + self.assertEqual(comp2["duration_ms"], self.comparisons[0].duration_ms) def test_authenticated_can_list_filtered(self): """ @@ -689,9 +696,13 @@ def test_authenticated_can_read(self): ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["entity_a"]["uid"], self._uid_01) - self.assertEqual(response.data["entity_b"]["uid"], self._uid_02) - self.assertEqual(response.data["duration_ms"], 102) + data = response.data + self.assertEqual(data["entity_a"]["uid"], self._uid_01) + self.assertEqual(data["entity_b"]["uid"], self._uid_02) + self.assertEqual(len(data["entity_a_contexts"]), 1) + self.assertEqual(data["entity_a_contexts"][0]["text"], self.ent_context_01_text.text) + self.assertEqual(data["entity_b_contexts"], []) + self.assertEqual(data["duration_ms"], 102) def test_authenticated_can_read_reverse(self): """ @@ -721,10 +732,15 @@ def test_authenticated_can_read_reverse(self): ), format="json", ) + + data = response.data self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["entity_a"]["uid"], self._uid_02) - self.assertEqual(response.data["entity_b"]["uid"], self._uid_01) - self.assertEqual(response.data["duration_ms"], 102) + self.assertEqual(data["entity_a"]["uid"], self._uid_02) + self.assertEqual(data["entity_b"]["uid"], self._uid_01) + self.assertEqual(data["entity_a_contexts"], []) + self.assertEqual(len(data["entity_b_contexts"]), 1) + self.assertEqual(data["entity_b_contexts"][0]["text"], self.ent_context_01_text.text) + self.assertEqual(data["duration_ms"], 102) def test_anonymous_cant_update(self): """ @@ -736,11 +752,7 @@ def test_anonymous_cant_update(self): self._uid_01, self._uid_02, ), - { - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ] - }, + {"criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}]}, format="json", ) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @@ -756,15 +768,69 @@ def test_authenticated_cant_update_non_existing_poll(self): "/users/me/comparisons/{}/{}/{}/".format( non_existing_poll, self._uid_01, self._uid_02 ), - { - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ] - }, + {"criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}]}, format="json", ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_authenticated_can_update(self): + self.client.force_authenticate(user=self.user) + + ent_context = EntityContext.objects.create( + name="context_safe_03", + origin=EntityContext.ASSOCIATION, + predicate={"video_id": self.videos[2].metadata["video_id"]}, + unsafe=False, + enabled=True, + poll=self.poll_videos, + ) + + ent_context_text = EntityContextLocale.objects.create( + context=ent_context, + language="en", + text="Hello context 03", + ) + + comparison1 = Comparison.objects.create( + poll=self.poll_videos, + user=self.user, + entity_1=self.videos[2], + entity_2=self.videos[3], + ) + comparison2 = Comparison.objects.create( + poll=self.poll_videos, + user=self.user, + entity_1=self.videos[1], + entity_2=self.videos[2], + ) + response = self.client.put( + "{}/{}/{}/".format( + self.comparisons_base_url, + self._uid_03, + self._uid_04, + ), + {"criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}]}, + format="json", + ) + + data = response.data + self.assertEqual(len(data["entity_a_contexts"]), 1) + self.assertEqual(data["entity_a_contexts"][0]["text"], ent_context_text.text) + self.assertEqual(data["entity_b_contexts"], []) + + response = self.client.get( + self.comparisons_base_url, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + comp1 = response.data["results"][0] + comp2 = response.data["results"][1] + self.assertEqual(comp1["entity_a"]["uid"], comparison1.entity_1.uid) + self.assertEqual(comp1["entity_b"]["uid"], comparison1.entity_2.uid) + self.assertEqual(comp2["entity_a"]["uid"], comparison2.entity_1.uid) + self.assertEqual(comp2["entity_b"]["uid"], comparison2.entity_2.uid) + def test_anonymous_cant_delete(self): """ An anonymous user can't delete a comparison. @@ -843,54 +909,6 @@ def test_authenticated_can_delete(self): entity_2=self.videos[1], ) - def test_authenticated_integrated_comparison_list(self): - self.client.force_authenticate(user=self.user) - comparison1 = Comparison.objects.create( - poll=self.poll_videos, - user=self.user, - entity_1=self.videos[2], - entity_2=self.videos[3], - ) - comparison2 = Comparison.objects.create( - poll=self.poll_videos, - user=self.user, - entity_1=self.videos[1], - entity_2=self.videos[2], - ) - self.client.put( - "{}/{}/{}/".format( - self.comparisons_base_url, - self._uid_03, - self._uid_04, - ), - { - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ] - }, - format="json", - ) - response = self.client.get( - self.comparisons_base_url, - format="json", - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - result_comparison1 = response.data["results"][0] - result_comparison2 = response.data["results"][1] - self.assertEqual( - result_comparison1["entity_a"]["uid"], comparison1.entity_1.uid - ) - self.assertEqual( - result_comparison1["entity_b"]["uid"], comparison1.entity_2.uid - ) - self.assertEqual( - result_comparison2["entity_a"]["uid"], comparison2.entity_1.uid - ) - self.assertEqual( - result_comparison2["entity_b"]["uid"], comparison2.entity_2.uid - ) - def test_n_ratings_from_video(self): self.client.force_authenticate(user=self.user) @@ -901,9 +919,7 @@ def test_n_ratings_from_video(self): data1 = { "entity_a": {"uid": self._uid_05}, "entity_b": {"uid": self._uid_06}, - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ], + "criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}], "duration_ms": 103, } response = self.client.post( @@ -916,9 +932,7 @@ def test_n_ratings_from_video(self): data2 = { "entity_a": {"uid": self._uid_05}, "entity_b": {"uid": self._uid_07}, - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ], + "criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}], "duration_ms": 103, } response = self.client.post( @@ -933,9 +947,7 @@ def test_n_ratings_from_video(self): data3 = { "entity_a": {"uid": self._uid_05}, "entity_b": {"uid": self._uid_06}, - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ], + "criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}], "duration_ms": 103, } response = self.client.post( @@ -958,9 +970,7 @@ def test_n_ratings_from_video(self): @patch("tournesol.utils.api_youtube.get_video_metadata") def test_metadata_refresh_on_comparison_creation(self, mock_get_video_metadata): - mock_get_video_metadata.return_value = { - "views": "42000" - } + mock_get_video_metadata.return_value = {"views": "42000"} user = UserFactory(username="non_existing_user") self.client.force_authenticate(user=user) @@ -976,9 +986,7 @@ def test_metadata_refresh_on_comparison_creation(self, mock_get_video_metadata): data = { "entity_a": {"uid": self._uid_01}, "entity_b": {"uid": self._uid_02}, - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ], + "criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}], } response = self.client.post(self.comparisons_base_url, data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) @@ -989,9 +997,7 @@ def test_metadata_refresh_on_comparison_creation(self, mock_get_video_metadata): data = { "entity_a": {"uid": self._uid_01}, "entity_b": {"uid": self._uid_03}, - "criteria_scores": [ - {"criteria": "largely_recommended", "score": 10, "weight": 10} - ], + "criteria_scores": [{"criteria": "largely_recommended", "score": 10, "weight": 10}], } response = self.client.post(self.comparisons_base_url, data, format="json") @@ -1086,18 +1092,9 @@ def test_update_individual_scores_after_new_comparison(self): resp = self.client.post( f"/users/me/comparisons/{self.poll.name}", data={ - "entity_a": { - "uid": self.entities[0].uid - }, - "entity_b": { - "uid": self.entities[2].uid - }, - "criteria_scores": [ - { - "criteria": "criteria1", - "score": 3 - } - ] + "entity_a": {"uid": self.entities[0].uid}, + "entity_b": {"uid": self.entities[2].uid}, + "criteria_scores": [{"criteria": "criteria1", "score": 3}], }, format="json", ) diff --git a/backend/tournesol/views/comparison.py b/backend/tournesol/views/comparison.py index 0aa6197902..ce89d95aa2 100644 --- a/backend/tournesol/views/comparison.py +++ b/backend/tournesol/views/comparison.py @@ -23,6 +23,19 @@ class InactivePollError(exceptions.PermissionDenied): class ComparisonApiMixin: """A mixin used to factorize behaviours common to all API views.""" + entity_contexts = None + + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + self.entity_contexts = self.poll_from_url.all_entity_contexts.prefetch_related( + "texts" + ).all() + + def get_serializer_context(self): + context = super().get_serializer_context() + context["entity_contexts"] = self.entity_contexts + return context + def comparison_already_exists(self, poll_id, request): """Return True if the comparison already exist, False instead.""" try: @@ -63,6 +76,7 @@ def get_queryset(self): Keyword arguments: uid -- the entity uid used to filter the results (default None) """ + queryset = ( Comparison.objects.select_related("entity_1", "entity_2") .prefetch_related("criteria_scores") @@ -82,6 +96,7 @@ class ComparisonListApi(mixins.CreateModelMixin, ComparisonListBaseApi): List all or a filtered list of comparisons made by the logged user, or create a new one. """ + def get(self, request, *args, **kwargs): """ Retrieve all comparisons made by the logged user, in a given poll. @@ -109,15 +124,11 @@ def perform_create(self, serializer): comparison.entity_1.update_entity_poll_rating(poll=poll) comparison.entity_1.inner.refresh_metadata() - comparison.entity_1.auto_remove_from_rate_later( - poll=poll, user=self.request.user - ) + comparison.entity_1.auto_remove_from_rate_later(poll=poll, user=self.request.user) comparison.entity_2.update_entity_poll_rating(poll=poll) comparison.entity_2.inner.refresh_metadata() - comparison.entity_2.auto_remove_from_rate_later( - poll=poll, user=self.request.user - ) + comparison.entity_2.auto_remove_from_rate_later(poll=poll, user=self.request.user) if settings.UPDATE_MEHESTAN_SCORES_ON_COMPARISON and poll.algorithm == ALGORITHM_MEHESTAN: update_user_scores(poll, user=self.request.user) @@ -152,7 +163,6 @@ class ComparisonDetailApi( DEFAULT_SERIALIZER = ComparisonSerializer UPDATE_SERIALIZER = ComparisonUpdateSerializer - currently_reversed = False def _select_serialization(self, straight=True): diff --git a/frontend/scripts/openapi.yaml b/frontend/scripts/openapi.yaml index 3a0c347f7c..b587f479da 100644 --- a/frontend/scripts/openapi.yaml +++ b/frontend/scripts/openapi.yaml @@ -2619,6 +2619,16 @@ components: $ref: '#/components/schemas/RelatedEntity' entity_b: $ref: '#/components/schemas/RelatedEntity' + entity_a_contexts: + type: array + items: + $ref: '#/components/schemas/EntityContext' + readOnly: true + entity_b_contexts: + type: array + items: + $ref: '#/components/schemas/EntityContext' + readOnly: true criteria_scores: type: array items: @@ -2631,7 +2641,9 @@ components: required: - criteria_scores - entity_a + - entity_a_contexts - entity_b + - entity_b_contexts ComparisonCriteriaScore: type: object properties: @@ -2739,10 +2751,22 @@ components: allOf: - $ref: '#/components/schemas/RelatedEntity' readOnly: true + entity_a_contexts: + type: array + items: + $ref: '#/components/schemas/EntityContext' + readOnly: true + entity_b_contexts: + type: array + items: + $ref: '#/components/schemas/EntityContext' + readOnly: true required: - criteria_scores - entity_a + - entity_a_contexts - entity_b + - entity_b_contexts ComparisonUpdateRequest: type: object description: |- diff --git a/frontend/src/components/entity/EntityCard.tsx b/frontend/src/components/entity/EntityCard.tsx index ef82c63e75..0ebe98ec0d 100644 --- a/frontend/src/components/entity/EntityCard.tsx +++ b/frontend/src/components/entity/EntityCard.tsx @@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next'; import { Box, Collapse, - Grid, IconButton, useTheme, useMediaQuery, Stack, Typography, } from '@mui/material'; +import Grid from '@mui/material/Unstable_Grid2'; import { ExpandMore as ExpandMoreIcon, ExpandLess as ExpandLessIcon, @@ -106,17 +106,26 @@ const EntityCard = ({ }; return ( - + {!isAvailable && ( - - + + {entity.type == TypeEnum.VIDEO ? t('video.notAvailableAnymore') : t('entityCard.thisElementIsNotAvailable')} - + {contentDisplayed ? ( @@ -130,15 +139,12 @@ const EntityCard = ({ {contentDisplayed && ( <> {displayContextAlert && unsafeContext && ( - + )} {showRatingControl && ( - + { return (
{ ); From aa2ca2a181d68d02e9220a2a2ebd8eccd85dccaf Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 4 Jan 2024 11:50:14 +0100 Subject: [PATCH 09/13] [ext] Prevent the merging of the 2 eslintrc files https://eslint.org/docs/latest/use/configure/configuration-files#cascading-and-hierarchy --- browser-extension/src/.eslintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/browser-extension/src/.eslintrc.json b/browser-extension/src/.eslintrc.json index 9ba923de8a..2659a2c24a 100644 --- a/browser-extension/src/.eslintrc.json +++ b/browser-extension/src/.eslintrc.json @@ -1,4 +1,5 @@ { + "root": true, "env": { "browser": true, "es6": true, From f7baf3154c3d320c86412290030bd26fe234d120 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 4 Jan 2024 12:27:08 +0100 Subject: [PATCH 10/13] [ext] Prevent `prepare` from being run by yarn The `prepare` script is run automatically by `yarn install`, so it may unexpectedly change the configuration. --- browser-extension/README.md | 4 ++-- browser-extension/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/browser-extension/README.md b/browser-extension/README.md index e9b58ab40a..19375adcdf 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -42,9 +42,9 @@ yarn install ### Prepare the extension -Before loading the extension into your browser, you need to run `yarn prepare`. It will generate `manifest.json`, `config.js` and the import wrappers (small scripts that allow us to use ECMAScript modules in content scripts). +Before loading the extension into your browser, you need to run `yarn configure`. It will generate `manifest.json`, `config.js` and the import wrappers (small scripts that allow us to use ECMAScript modules in content scripts). -By default, the script creates an extension that connects to the production Tournesol website. If you want to connect to your development servers, you can run `yarn prepare:dev` instead. +By default, the script creates an extension that connects to the production Tournesol website. If you want to connect to your development servers, you can run `yarn configure:dev` instead. ### Code Quality diff --git a/browser-extension/package.json b/browser-extension/package.json index 1a3fa3f7df..e3487eca5a 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -4,8 +4,8 @@ "license": "AGPL-3.0-or-later", "type": "module", "scripts": { - "prepare": "node prepareExtension.js", - "prepare:dev": "TOURNESOL_ENV=dev-env node prepareExtension.js", + "configure": "node prepareExtension.js", + "configure:dev": "TOURNESOL_ENV=dev-env node prepareExtension.js", "lint": "eslint .", "lint:fix": "eslint --fix ." }, From 6be429493c28b2f731908c832269e10c55cde886 Mon Sep 17 00:00:00 2001 From: Michael Witrant Date: Thu, 4 Jan 2024 12:30:54 +0100 Subject: [PATCH 11/13] [ext] Bump the version to 3.4.1 --- browser-extension/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-extension/package.json b/browser-extension/package.json index e3487eca5a..5ca40c5dbe 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,6 +1,6 @@ { "name": "tournesol-extension", - "version": "3.4.0", + "version": "3.4.1", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { From f3dd7f656266bb5f03722a2576a5121e4c1c9398 Mon Sep 17 00:00:00 2001 From: Adrien Matissart Date: Thu, 4 Jan 2024 16:12:00 +0100 Subject: [PATCH 12/13] [ext] fix: avoid repeated calls to initializeHomeRecommendations and initializeSearchRecommendations (#1874) --- browser-extension/package.json | 2 +- browser-extension/src/displayHomeRecommendations.js | 11 +++++------ .../src/displaySearchRecommendations.js | 12 +++++------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/browser-extension/package.json b/browser-extension/package.json index 5ca40c5dbe..e8c7cdbc50 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,6 +1,6 @@ { "name": "tournesol-extension", - "version": "3.4.1", + "version": "3.4.2", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { diff --git a/browser-extension/src/displayHomeRecommendations.js b/browser-extension/src/displayHomeRecommendations.js index 9084b7431d..041cb6fd33 100644 --- a/browser-extension/src/displayHomeRecommendations.js +++ b/browser-extension/src/displayHomeRecommendations.js @@ -23,22 +23,21 @@ parentComponentQuery: '#primary > ytd-rich-grid-renderer', displayCriteria: false, }); - homeRecommendations = new TournesolRecommendations(options); + return new TournesolRecommendations(options); }; const processHomeRecommendations = async () => { if (homeRecommendations === undefined) { - await initializeHomeRecommendations(); + homeRecommendations = initializeHomeRecommendations(); } - - homeRecommendations.process(); + (await homeRecommendations).process(); }; - const clearHomeRecommendations = () => { + const clearHomeRecommendations = async () => { if (homeRecommendations === undefined) { return; } - homeRecommendations.clear(); + (await homeRecommendations).clear(); }; const process = () => { diff --git a/browser-extension/src/displaySearchRecommendations.js b/browser-extension/src/displaySearchRecommendations.js index 826e673477..4805136969 100644 --- a/browser-extension/src/displaySearchRecommendations.js +++ b/browser-extension/src/displaySearchRecommendations.js @@ -26,23 +26,21 @@ displayCriteria: true, }); - searchRecommendations = new TournesolSearchRecommendations(options); + return new TournesolSearchRecommendations(options); }; const processSearchRecommendations = async () => { if (searchRecommendations === undefined) { - await initializeSearchRecommendations(); + searchRecommendations = initializeSearchRecommendations(); } - - searchRecommendations.process(); + (await searchRecommendations).process(); }; - const clearSearchRecommendations = () => { + const clearSearchRecommendations = async () => { if (searchRecommendations === undefined) { return; } - - searchRecommendations.clear(); + (await searchRecommendations).clear(); }; // Allow to display the Tournesol search results without modifying the From c6419a81667895aad8ad285f21cc313fcb291c00 Mon Sep 17 00:00:00 2001 From: Gresille & Siffle <39056254+GresilleSiffle@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:39:45 +0100 Subject: [PATCH 13/13] [front] feat: display entity context Chip in the entity selector (#1870) --- frontend/public/locales/en/translation.json | 3 ++ frontend/public/locales/fr/translation.json | 3 ++ frontend/src/components/entity/EntityCard.tsx | 18 ++++++-- .../components/entity/EntityContextChip.tsx | 46 +++++++++++++++++++ .../entity/EntityIndividualScores.tsx | 38 +++++++-------- .../src/features/comparisons/Comparison.tsx | 8 +--- 6 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 frontend/src/components/entity/EntityContextChip.tsx diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 5aa3500cea..c8f6e6f8b7 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -44,6 +44,9 @@ "score": { "totalScoreBasedOnRankingChoice": "Score based on selected ranking parameters" }, + "entityContextChip": { + "context": "context" + }, "entityIndividualScores": { "inYourOpinion": "your score <1>" }, diff --git a/frontend/public/locales/fr/translation.json b/frontend/public/locales/fr/translation.json index adc0bba939..4928580dd9 100644 --- a/frontend/public/locales/fr/translation.json +++ b/frontend/public/locales/fr/translation.json @@ -47,6 +47,9 @@ "score": { "totalScoreBasedOnRankingChoice": "Score basé sur les critères de classement sélectionnés" }, + "entityContextChip": { + "context": "contexte" + }, "entityIndividualScores": { "inYourOpinion": "selon vous <1>" }, diff --git a/frontend/src/components/entity/EntityCard.tsx b/frontend/src/components/entity/EntityCard.tsx index 0ebe98ec0d..ad3295d2af 100644 --- a/frontend/src/components/entity/EntityCard.tsx +++ b/frontend/src/components/entity/EntityCard.tsx @@ -34,6 +34,7 @@ import { RatingControl } from 'src/features/ratings/RatingControl'; import EntityCardTitle from './EntityCardTitle'; import EntityCardScores from './EntityCardScores'; +import EntityContextChip from './EntityContextChip'; import EntityImagery from './EntityImagery'; import EntityMetadata, { VideoMetadata } from './EntityMetadata'; import EntityIndividualScores from './EntityIndividualScores'; @@ -241,10 +242,12 @@ export const RowEntityCard = ({ result, withLink = false, individualScores, + displayEntityContextChip = true, }: { result: EntityResult; withLink?: boolean; individualScores?: ContributorCriteriaScore[]; + displayEntityContextChip?: boolean; }) => { const entity = result.entity; return ( @@ -283,9 +286,18 @@ export const RowEntityCard = ({ withLinks={false} /> )} - {individualScores && ( - - )} + + + {displayEntityContextChip && 'entity_contexts' in result && ( + + )} + {individualScores && ( + + )} + ); diff --git a/frontend/src/components/entity/EntityContextChip.tsx b/frontend/src/components/entity/EntityContextChip.tsx new file mode 100644 index 0000000000..fe391b1d9d --- /dev/null +++ b/frontend/src/components/entity/EntityContextChip.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; + +import { Chip } from '@mui/material'; + +import { EntityContext, OriginEnum } from 'src/services/openapi'; + +export const EntityContextChip = ({ + uid, + entityContexts, +}: { + uid: string; + entityContexts: EntityContext[]; +}) => { + const history = useHistory(); + const { t } = useTranslation(); + + const handleClick = (event: React.MouseEvent) => { + event.stopPropagation(); + history.push(`/entities/${uid}#entity-context`); + }; + + const unsafeContext = entityContexts.find( + (context) => context.unsafe && context.origin === OriginEnum.ASSOCIATION + ); + + if (unsafeContext == undefined) { + return <>; + } + + return ( + + ); +}; + +export default EntityContextChip; diff --git a/frontend/src/components/entity/EntityIndividualScores.tsx b/frontend/src/components/entity/EntityIndividualScores.tsx index 8145481d10..ca2afe3430 100644 --- a/frontend/src/components/entity/EntityIndividualScores.tsx +++ b/frontend/src/components/entity/EntityIndividualScores.tsx @@ -28,26 +28,26 @@ export const EntityIndividualScores = ({ )?.score; } + if (mainCriterionScore == undefined) { + return <>; + } + return ( - - {mainCriterionScore != null && ( - - - in your opinion - - - - } - /> - )} - + + + in your opinion + + + + } + /> ); }; diff --git a/frontend/src/features/comparisons/Comparison.tsx b/frontend/src/features/comparisons/Comparison.tsx index 3ea75612d1..fd50a6c773 100644 --- a/frontend/src/features/comparisons/Comparison.tsx +++ b/frontend/src/features/comparisons/Comparison.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Location } from 'history';