Skip to content

Commit

Permalink
Merge branch 'main' into 1983-new_reco_pages
Browse files Browse the repository at this point in the history
  • Loading branch information
GresilleSiffle committed Sep 26, 2024
2 parents aeb6d54 + 97bee8f commit 07993e7
Show file tree
Hide file tree
Showing 34 changed files with 2,106 additions and 1,953 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
spec: "cypress/e2e/browser-extension/**/*"
config: baseUrl=https://www.youtube.com

- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
working-directory: ./dev-env
run: docker-compose logs -t --tail=300

- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots
Expand Down
2 changes: 1 addition & 1 deletion backend/core/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-07-30 09:53+0000\n"
"POT-Creation-Date: 2024-08-06 08:43+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Full stack Web Framework used for Tournesol's backend
# https://docs.djangoproject.com
Django==4.2.15
Django==4.2.16
# Used for fields computed on save for Django models
# See https://github.com/brechin/django-computed-property/
django-computed-property==0.3.0
Expand Down
19 changes: 18 additions & 1 deletion backend/tournesol/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-18 15:23+0000\n"
"POT-Creation-Date: 2024-08-06 08:43+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand All @@ -35,6 +35,23 @@ msgstr "Oui"
msgid "No"
msgstr "Non"

#: tournesol/models/comparisons.py:155
msgid "The value of score_max cannot be None."
msgstr "La valeur de score_max ne peut pas être None."

#: tournesol/models/comparisons.py:158
msgid "The value of score_max must be greater than or equal to 1."
msgstr "La valeur de score_max doit être supérieure ou égale à 1."

#: tournesol/models/comparisons.py:163
#, python-format
msgid ""
"The absolute value of the score %(score)s given to the criterion "
"%(criterion)s can't be greater than the value of score_max %(score_max)s."
msgstr ""
"La valeur absolue du score %(score)s donnée au critère "
"%(criterion)s ne peut pas être supérieure à la valeur de score_max %(score_max)s."

#: tournesol/serializers/rate_later.py:60
msgid "The entity is already in the rate-later list of this poll."
msgstr "L'entité est déjà dans la liste à comparer plus tard de ce scrutin."
Expand Down
25 changes: 15 additions & 10 deletions backend/tournesol/models/comparisons.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import F, ObjectDoesNotExist, Q
from django.utils.translation import gettext_lazy as _

from core.models import User

Expand Down Expand Up @@ -148,25 +149,29 @@ class Meta:
def __str__(self):
return f"{self.comparison}/{self.criteria}/{self.score}"

def _validate_score_max(self):
if self.score_max is None:
raise TypeError("The value of score_max cannot be None.")
@staticmethod
def validate_score_max(score: float, score_max: int, criterion: str):
if score_max is None:
raise TypeError(_("The value of score_max cannot be None."))

if self.score_max <= 0:
raise ValueError("The value of score_max must be greater than or equal to 1.")
if score_max <= 0:
raise ValueError(_("The value of score_max must be greater than or equal to 1."))

if abs(self.score) > self.score_max:
if abs(score) > score_max:
raise ValueError(
f"The absolute value of the score {self.score} given to the criterion "
f"{self.criteria} can't be greater than the value of score_max {self.score_max}."
_(
"The absolute value of the score %(score)s given to the criterion "
"%(criterion)s can't be greater than the value of score_max %(score_max)s."
)
% {"score": score, "criterion": criterion, "score_max": score_max}
)

def clean(self):
try:
self._validate_score_max()
self.validate_score_max(self.score, self.score_max, self.criteria)
except (TypeError, ValueError) as err:
raise ValidationError({"score_max": err.args[0]}) from err

def save(self, *args, **kwargs):
self._validate_score_max()
self.validate_score_max(self.score, self.score_max, self.criteria)
return super().save(*args, **kwargs)
11 changes: 11 additions & 0 deletions backend/tournesol/serializers/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ def validate_criteria(self, value):
)
return value

def validate(self, attrs):
try:
ComparisonCriteriaScore.validate_score_max(
attrs["score"],
attrs["score_max"],
attrs["criteria"],
)
except (TypeError, ValueError) as err:
raise ValidationError({"score_max": err.args[0]}) from err
return attrs


class ComparisonSerializerMixin:
def format_entity_contexts(self, poll, contexts, metadata):
Expand Down
58 changes: 58 additions & 0 deletions backend/tournesol/tests/test_api_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,28 @@ def test_authenticated_cant_create_reverse(self):
initial_comparisons_nbr + 1,
)

def test_authenticated_cant_create_invalid_score_max(self):
"""
An authenticated user can't create a comparison with a criterion whose
score is higher than score_max.
"""
initial_comparisons_nbr = Comparison.objects.all().count()
data = deepcopy(self.non_existing_comparison)
data["criteria_scores"][0]["score"] = 10
data["criteria_scores"][0]["score_max"] = 5

self.client.force_authenticate(user=self.user)
response = self.client.post(
self.comparisons_base_url,
data,
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
Comparison.objects.all().count(),
initial_comparisons_nbr,
)

def test_anonymous_cant_list(self):
"""
An anonymous user can't list its comparisons.
Expand Down Expand Up @@ -988,6 +1010,7 @@ def test_authenticated_can_patch_optional(self):
{
"criteria": optional,
"score": 4,
"score_max": 8,
}
]
},
Expand Down Expand Up @@ -1061,6 +1084,41 @@ def test_authenticated_can_patch_several(self):
],
)

def test_authenticated_cant_patch_invalid_score_max(self):
"""
An authenticated user can't patch the score of a criterion with a
value higher than score_max.
"""
self.client.force_authenticate(user=self.user)
self.comparisons[0].criteria_scores.all().delete()

ComparisonCriteriaScore.objects.create(
comparison=self.comparisons[0],
criteria="largely_recommended",
score=8,
score_max=10,
)

response = self.client.patch(
"{}/{}/{}/".format(
self.comparisons_base_url,
self._uid_01,
self._uid_02,
),
data={
"criteria_scores": [
{
"criteria": "largely_recommended",
"score": 10,
"score_max": 8,
}
]
},
format="json",
)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_anonymous_cant_delete(self):
"""
An anonymous user can't delete a comparison.
Expand Down
76 changes: 56 additions & 20 deletions backend/tournesol/tests/test_model_comparisoncriteriascore.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,44 @@ def test_validate_score_max(self):

# The score cannot be greater than score_max.
with self.assertRaises(ValueError):
score_test._validate_score_max()
score_test.validate_score_max(
score=score_test.score,
score_max=score_test.score_max,
criterion=score_test.criteria,
)

score_test.score = -11
# The absolute value of the score cannot be greater than score_max.
with self.assertRaises(ValueError):
score_test._validate_score_max()
score_test.validate_score_max(
score=score_test.score,
score_max=score_test.score_max,
criterion=score_test.criteria,
)

# The score can be zero.
score_test.score = 0
score_test._validate_score_max()
score_test.validate_score_max(
score=score_test.score,
score_max=score_test.score_max,
criterion=score_test.criteria,
)

# The score can be equal to the score_max.
score_test.score = score_test.score_max
score_test._validate_score_max()
score_test.validate_score_max(
score=score_test.score,
score_max=score_test.score_max,
criterion=score_test.criteria,
)

# The absolute value of the score can be lesser than score_max.
score_test.score = -1
score_test._validate_score_max()
score_test.validate_score_max(
score=score_test.score,
score_max=score_test.score_max,
criterion=score_test.criteria,
)

score_max_test = ComparisonCriteriaScore(
comparison=self.comparison,
Expand All @@ -83,24 +103,40 @@ def test_validate_score_max(self):

# score_max cannot be None.
with self.assertRaises(TypeError):
score_max_test._validate_score_max()
score_max_test.validate_score_max(
score=score_max_test.score,
score_max=score_max_test.score_max,
criterion=score_max_test.criteria,
)

score_max_test.score_max = 0
# score_max cannot be zero.
with self.assertRaises(ValueError):
score_max_test._validate_score_max()
score_max_test.validate_score_max(
score=score_max_test.score,
score_max=score_max_test.score_max,
criterion=score_max_test.criteria,
)

score_max_test.score_max = -10
# score_max cannot be negative.
with self.assertRaises(ValueError):
score_max_test._validate_score_max()
score_max_test.validate_score_max(
score=score_max_test.score,
score_max=score_max_test.score_max,
criterion=score_max_test.criteria,
)

score_max_test.score_max = 10
score_max_test._validate_score_max()
score_max_test.validate_score_max(
score=score_max_test.score,
score_max=score_max_test.score_max,
criterion=score_max_test.criteria,
)

def test_clean_calls_validate_score_max(self):
"""
The method `clean` should call the method `_validate_score_max` and
The method `clean` should call the method `validate_score_max` and
transform its exceptions into `ValidationError`.
"""
score = ComparisonCriteriaScore(
Expand All @@ -110,27 +146,27 @@ def test_clean_calls_validate_score_max(self):
score_max=10,
)

score._validate_score_max = Mock()
score.validate_score_max = Mock()
score.clean()
score._validate_score_max.assert_called_once()
score.validate_score_max.assert_called_once()

# The exceptions raised by _validate_score_max should be transformed
# The exceptions raised by validate_score_max should be transformed
# into `ValidationError` by the method `clean`.
expected_exceptions = [TypeError("oops"), ValueError("oops")]
for exception in expected_exceptions:
score._validate_score_max = Mock(side_effect=exception)
score.validate_score_max = Mock(side_effect=exception)

with self.assertRaises(ValidationError):
score.clean()

# All other exceptions should not be transformed.
score._validate_score_max = Mock(side_effect=KeyError)
score.validate_score_max = Mock(side_effect=KeyError)
with self.assertRaises(KeyError):
score.clean()

def test_save_calls_validate_score_max(self):
"""
The method `save` should call the method `_validate_score_max`.
The method `save` should call the method `validate_score_max`.
"""
score = ComparisonCriteriaScore(
comparison=self.comparison,
Expand All @@ -139,15 +175,15 @@ def test_save_calls_validate_score_max(self):
score_max=10,
)

score._validate_score_max = Mock()
score.validate_score_max = Mock()
score.save()
score._validate_score_max.assert_called_once()
score.validate_score_max.assert_called_once()

# All exceptions raised by _validate_score_max should not be
# All exceptions raised by validate_score_max should not be
# transformed by `save`.
expected_exceptions = [KeyError, TypeError, ValueError]
for exception in expected_exceptions:
score._validate_score_max = Mock(side_effect=exception)
score.validate_score_max = Mock(side_effect=exception)

with self.assertRaises(exception):
score.save()
2 changes: 1 addition & 1 deletion data-visualization/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
streamlit==1.35.0
streamlit==1.37.0
numpy==1.24.2
pandas==1.5.3
seaborn==0.11.1
Expand Down
2 changes: 1 addition & 1 deletion dev-env/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ services:
condition: service_healthy

front:
image: node:18-bullseye-slim
image: node:18.20-bullseye-slim
container_name: tournesol-dev-front
working_dir: /frontend
entrypoint: bash -c
Expand Down
Loading

0 comments on commit 07993e7

Please sign in to comment.