From a732edfee6c2c3a09c283ff0c93f3f624aad63a8 Mon Sep 17 00:00:00 2001 From: Matthew McKenzie <97194636+mattjamc@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:31:00 +0000 Subject: [PATCH 1/4] TP2000-1678 Measure condition duty bug update (#1395) * Duty sentence parser return Decimal * Validate decimal places and return error * Add test for new error message * Move validation to duty sentence parser * Update test * Remove form validation again after master merge * Robust decimal detection * Consolidate test into another --- measures/duty_sentence_parser.py | 9 ++++++++ measures/forms/mixins.py | 7 ------ measures/tests/test_forms.py | 37 ++++++-------------------------- 3 files changed, 15 insertions(+), 38 deletions(-) diff --git a/measures/duty_sentence_parser.py b/measures/duty_sentence_parser.py index dc2845447..c776abea8 100644 --- a/measures/duty_sentence_parser.py +++ b/measures/duty_sentence_parser.py @@ -379,6 +379,12 @@ def validate_measurement(self, unit, qualifier): f"Measurement unit qualifier {qualifier.abbreviation} cannot be used with measurement unit {unit.abbreviation}.", ) + def validate_duty_amount(self, duty_amount): + if duty_amount.as_tuple().exponent < -3: + raise ValidationError( + f"The reference price cannot have more than 3 decimal places.", + ) + def validate_phrase(self, phrase): # Each measure component can have an amount, monetary unit and measurement. # Which expression elements are allowed in a component is controlled by @@ -393,6 +399,9 @@ def validate_phrase(self, phrase): measurement_unit = phrase.get("measurement_unit", None) measurement_unit_qualifier = phrase.get("measurement_unit_qualifier", None) + if duty_amount: + self.validate_duty_amount(duty_amount) + self.validate_according_to_applicability_code( amount_code, duty_expression, diff --git a/measures/forms/mixins.py b/measures/forms/mixins.py index 2494ae996..6bf3a2acc 100644 --- a/measures/forms/mixins.py +++ b/measures/forms/mixins.py @@ -252,13 +252,6 @@ def conditions_clean( try: components = parser.transform(price) cleaned_data["duty_amount"] = components[0].get("duty_amount") - if ( - cleaned_data["duty_amount"] - and len(str(cleaned_data["duty_amount"]).split(".")[-1]) > 3 - ): - raise ValidationError( - f"The reference price cannot have more than 3 decimal places.", - ) cleaned_data["monetary_unit"] = components[0].get("monetary_unit") cleaned_data["condition_measurement"] = ( models.Measurement.objects.as_at(date) diff --git a/measures/tests/test_forms.py b/measures/tests/test_forms.py index 82ad6a7a7..032f1efa9 100644 --- a/measures/tests/test_forms.py +++ b/measures/tests/test_forms.py @@ -1025,6 +1025,10 @@ def test_measure_forms_conditions_invalid_duty( "3.5 % + 11 GBP / 100 kg", "A compound duty expression was found at character 7. \n\nCheck that you are entering a single duty amount or a duty amount together with a measurement unit (and measurement unit qualifier if required). ", ), + ( + "1.2345 GBP / kg", + "The reference price cannot have more than 3 decimal places.", + ), ], ) def test_measure_forms_conditions_wizard_invalid_duty( @@ -1220,37 +1224,6 @@ def test_measure_forms_conditions_wizard_clears_unneeded_certificate(date_ranges assert form_expects_no_certificate.cleaned_data["required_certificate"] is None -def test_measure_forms_conditions_wizard_form_invalid_duty(date_ranges): - """Tests that MeasureConditionsWizardStepForm is invalid when the duty has - more than 3 decimal places.""" - condition_code = factories.MeasureConditionCodeFactory.create() - monetary_unit = factories.MonetaryUnitFactory.create() - factories.MeasurementUnitFactory.create() - factories.MeasurementUnitQualifierFactory.create() - factories.MeasureConditionComponentFactory.create() - factories.DutyExpressionFactory.create(sid=99) - action = factories.MeasureActionFactory.create() - - data = { - "reference_price": f"1.2345 {monetary_unit.code}", - "action": action.pk, - "condition_code": condition_code.pk, - } - # MeasureConditionsForm.__init__ expects prefix kwarg for instantiating crispy forms `Layout` object - form = forms.MeasureConditionsWizardStepForm( - data, - prefix="", - measure_start_date=date_ranges.normal, - ) - - with override_current_transaction(action.transaction): - assert not form.is_valid() - assert ( - "The reference price cannot have more than 3 decimal places." - in form.errors["reference_price"] - ) - - def test_measure_form_valid_data(erga_omnes, session_request_with_workbasket): """Test that MeasureForm.is_valid returns True when passed required fields and geographical_area and sid fields in cleaned data.""" @@ -2003,6 +1976,8 @@ def test_simple_measure_edit_forms_serialize_deserialize( ): """Test that the EditMeasure simple forms that use the SerializableFormMixin behave correctly and as expected.""" + """Test that the EditMeasure simple forms that use the SerializableFormMixin + behave correctly and as expected.""" # Create some measures to apply this data to, for the kwargs quota_order_number = factories.QuotaOrderNumberFactory() From 2cfaeb218d6bdd8568c59ad575dd7d502b844e3e Mon Sep 17 00:00:00 2001 From: Dale Cannon <118175145+dalecannon@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:15:02 +0000 Subject: [PATCH 2/4] Revert "TP2000-1577 Correct usage of select_for_update() (#1358)" (#1397) This reverts commit bc3ea8813a26a6fafbce25425367ee777495bc08. --- publishing/models/packaged_workbasket.py | 131 ++++++-------- publishing/tests/test_models.py | 218 ----------------------- 2 files changed, 51 insertions(+), 298 deletions(-) diff --git a/publishing/models/packaged_workbasket.py b/publishing/models/packaged_workbasket.py index 40e1d3148..cc24edfa9 100644 --- a/publishing/models/packaged_workbasket.py +++ b/publishing/models/packaged_workbasket.py @@ -31,7 +31,6 @@ from notifications.models import EnvelopeRejectedNotification from notifications.models import NotificationLog from publishing import models as publishing_models -from publishing.models.decorators import refresh_after from publishing.models.decorators import save_after from publishing.models.decorators import skip_notifications_if_disabled from publishing.models.state import ProcessingState @@ -438,7 +437,6 @@ def begin_processing_condition_at_position_1(self) -> bool: """Django FSM condition: Instance must be at position 1 in order to complete the begin_processing transition to CURRENTLY_PROCESSING.""" - self.refresh_from_db(fields=["position"]) return self.position == 1 def begin_processing_condition_no_instances_currently_processing(self) -> bool: @@ -478,11 +476,9 @@ def begin_processing(self): multiple instances it's necessary for this method to perform a save() operation upon successful transitions. """ - instance = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - pk=self.pk, - ) - instance.processing_started_at = make_aware(datetime.now()) - instance.save() + PackagedWorkBasket.objects.select_for_update(nowait=True).get(pk=self.pk) + self.processing_started_at = make_aware(datetime.now()) + self.save() @create_envelope_on_completed_processing @save_after @@ -622,7 +618,6 @@ def cds_notified_notification_log(self) -> NotificationLog: @atomic @create_envelope_on_new_top - @refresh_after def pop_top(self) -> "PackagedWorkBasket": """ Pop the top-most instance, shuffling all remaining queued instances @@ -631,34 +626,23 @@ def pop_top(self) -> "PackagedWorkBasket": Management of the popped instance's `processing_state` is not altered by this function and should be managed separately by the caller. """ - - instance = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - pk=self.pk, - ) - - if instance.position != 1: + if self.position != 1: raise PackagedWorkBasketInvalidQueueOperation( - "Unable to pop instance at position {instance.position} in queue " + "Unable to pop instance at position {self.position} in queue " "because it is not at position 1.", ) - instance.position = 0 - instance.save() - - to_update = list( - PackagedWorkBasket.objects.select_for_update(nowait=True) - .filter(position__gt=1) - .values_list("pk", flat=True), - ) - PackagedWorkBasket.objects.filter(pk__in=to_update).update( + PackagedWorkBasket.objects.select_for_update(nowait=True).filter( + position__gt=0, + ).update( position=F("position") - 1, ) + self.refresh_from_db() - return instance + return self @atomic @create_envelope_on_new_top - @refresh_after def remove_from_queue(self) -> "PackagedWorkBasket": """ Remove instance from the queue, shuffling all successive queued @@ -668,111 +652,98 @@ def remove_from_queue(self) -> "PackagedWorkBasket": this function and should be managed separately by the caller. """ - instance = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - pk=self.pk, - ) + PackagedWorkBasket.objects.select_for_update(nowait=True).get(pk=self.pk) + self.refresh_from_db() - if instance.position == 0: + if self.position == 0: raise PackagedWorkBasketInvalidQueueOperation( "Unable to remove instance with a position value of 0 from " "queue because 0 indicates that it is not a queue member.", ) - current_position = instance.position - instance.position = 0 - instance.save() + current_position = self.position + self.position = 0 + self.save() - to_update = list( - PackagedWorkBasket.objects.select_for_update(nowait=True) - .filter(position__gt=current_position) - .values_list("pk", flat=True), - ) - PackagedWorkBasket.objects.filter(pk__in=to_update).update( + PackagedWorkBasket.objects.select_for_update(nowait=True).filter( + position__gt=current_position, + ).update( position=F("position") - 1, ) + self.refresh_from_db() - return instance + return self @atomic @create_envelope_on_new_top - @refresh_after def promote_to_top_position(self) -> "PackagedWorkBasket": """Promote the instance to the top position of the package processing queue so that it occupies position 1.""" - instance = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - pk=self.pk, - ) + PackagedWorkBasket.objects.select_for_update(nowait=True).get(pk=self.pk) + self.refresh_from_db() - if instance.position <= 1: - return instance + if self.position <= 1: + return self - current_position = instance.position + position = self.position - to_update = list( - PackagedWorkBasket.objects.select_for_update(nowait=True) - .filter(Q(position__gte=1) & Q(position__lt=current_position)) - .values_list("pk", flat=True), - ) - PackagedWorkBasket.objects.filter(pk__in=to_update).update( - position=F("position") + 1, - ) + PackagedWorkBasket.objects.select_for_update(nowait=True).filter( + Q(position__gte=1) & Q(position__lt=position), + ).update(position=F("position") + 1) - instance.position = 1 - instance.save() + self.position = 1 + self.save() + self.refresh_from_db() - return instance + return self @atomic @create_envelope_on_new_top - @refresh_after def promote_position(self) -> "PackagedWorkBasket": """Promote the instance by one position up the package processing queue.""" - instance = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - pk=self.pk, - ) + PackagedWorkBasket.objects.select_for_update(nowait=True).get(pk=self.pk) + self.refresh_from_db() - if instance.position <= 1: - return instance + if self.position <= 1: + return self obj_to_swap = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - position=instance.position - 1, + position=self.position - 1, ) obj_to_swap.position += 1 - instance.position -= 1 - + self.position -= 1 PackagedWorkBasket.objects.bulk_update( - [instance, obj_to_swap], + [self, obj_to_swap], ["position"], ) + self.refresh_from_db() - return instance + return self @atomic @create_envelope_on_new_top - @refresh_after def demote_position(self) -> "PackagedWorkBasket": """Demote the instance by one position down the package processing queue.""" - instance = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - pk=self.pk, - ) + PackagedWorkBasket.objects.select_for_update(nowait=True).get(pk=self.pk) + self.refresh_from_db() - if instance.position in {0, PackagedWorkBasket.objects.max_position()}: - return instance + if self.position in {0, PackagedWorkBasket.objects.max_position()}: + return self obj_to_swap = PackagedWorkBasket.objects.select_for_update(nowait=True).get( - position=instance.position + 1, + position=self.position + 1, ) obj_to_swap.position -= 1 - instance.position += 1 - + self.position += 1 PackagedWorkBasket.objects.bulk_update( - [instance, obj_to_swap], + [self, obj_to_swap], ["position"], ) + self.refresh_from_db() - return instance + return self diff --git a/publishing/tests/test_models.py b/publishing/tests/test_models.py index 2624523ca..dd7371d6f 100644 --- a/publishing/tests/test_models.py +++ b/publishing/tests/test_models.py @@ -1,5 +1,3 @@ -import threading -from functools import wraps from unittest import mock from unittest.mock import MagicMock from unittest.mock import patch @@ -7,7 +5,6 @@ import factory import freezegun import pytest -from django.db import OperationalError from django_fsm import TransitionNotAllowed from common.tests import factories @@ -472,221 +469,6 @@ def test_next_envelope_id(envelope_storage): assert Envelope.next_envelope_id() == "230002" -@pytest.mark.django_db(transaction=True) -class TestPackagingQueueRaceConditions: - """Tests that concurrent requests to reorder packaged workbaskets don't - result in duplicate or non-consecutive positions.""" - - NUM_THREADS: int = 2 - """The number of threads each test uses.""" - - THREAD_TIMEOUT: int = 5 - """The duration in seconds to wait for a thread to complete before timing - out.""" - - NUM_PACKAGED_WORKBASKETS: int = 5 - """The number of packaged workbaskets to create for each test.""" - - @pytest.fixture(autouse=True) - def setup(self, settings): - """Initialises a barrier to synchronise threads and creates packaged - workbaskets anew for each test.""" - settings.ENABLE_PACKAGING_NOTIFICATIONS = False - - self.unexpected_exceptions = [] - - self.barrier = threading.Barrier( - parties=self.NUM_THREADS, - timeout=self.THREAD_TIMEOUT, - ) - - for _ in range(self.NUM_PACKAGED_WORKBASKETS): - self._create_packaged_workbasket() - - self.packaged_workbaskets = PackagedWorkBasket.objects.filter( - processing_state__in=ProcessingState.queued_states(), - ) - - def _create_packaged_workbasket(self): - """Creates a new packaged workbasket with a unique - create_envelope_task_id.""" - with patch( - "publishing.tasks.create_xml_envelope_file.apply_async", - return_value=MagicMock(id=factory.Faker("uuid4")), - ): - factories.QueuedPackagedWorkBasketFactory() - - def assert_no_unexpected_exceptions(self): - """Asserts that no threads raised an unexpected exception.""" - assert ( - not self.unexpected_exceptions - ), f"Unexpected exception(s) raised: {self.unexpected_exceptions}" - - def assert_expected_positions(self): - """Asserts that positions in the packaging queue are both unique and in - consecutive sequence.""" - positions = list( - PackagedWorkBasket.objects.filter( - processing_state__in=ProcessingState.queued_states(), - ) - .order_by("position") - .values_list("position", flat=True), - ) - - assert len(set(positions)) == len(positions), "Duplicate positions found!" - - assert positions == list( - range(min(positions), max(positions) + 1), - ), "Non-consecutive positions found!" - - def synchronised(func): - """ - Decorator that ensures all threads wait until they can call their target - function in a synchronised fashion. - - Any unexpected exceptions raised during the execution of the decorated - function are stored for the individual test to re-raise. - """ - - @wraps(func) - def wrapper(self, *args, **kwargs): - try: - self.barrier.wait() - func(self, *args, **kwargs) - except (TransitionNotAllowed, OperationalError): - pass - except Exception as error: - self.unexpected_exceptions.append(error) - - return wrapper - - @synchronised - def synchronised_call( - self, - method_name: str, - packaged_workbasket: PackagedWorkBasket, - ): - """ - Thread-synchronised wrapper for the following `PackagedWorkBasket` - - instance methods: - - begin_processing - - abandon - - promote_to_top_position - - promote - - demote - """ - getattr(packaged_workbasket, method_name)() - - @synchronised - def synchronised_create_packaged_workbasket(self): - """Thread-synchronised wrapper method to create a new - `PackagedWorkbasket` instance.""" - self._create_packaged_workbasket() - - def execute_threads(self, threads: list[threading.Thread]): - """Starts a list of threads and waits for them to complete or - timeout.""" - for thread in threads: - thread.start() - - for thread in threads: - thread.join(timeout=self.THREAD_TIMEOUT) - if thread.is_alive(): - raise RuntimeError(f"Thread {thread.name} timed out.") - - def test_process_and_promote_to_top_packaged_workbaskets(self): - """Begins processing the top-most packaged workbasket while promoting to - the top the packaged workbasket in last place.""" - thread1 = threading.Thread( - target=self.synchronised_call, - kwargs={ - "method_name": "begin_processing", - "packaged_workbasket": self.packaged_workbaskets[0], - }, - name="BeginProcessingThread1", - ) - thread2 = threading.Thread( - target=self.synchronised_call, - kwargs={ - "method_name": "promote_to_top_position", - "packaged_workbasket": self.packaged_workbaskets[4], - }, - name="PromoteToTopThread2", - ) - - self.execute_threads(threads=[thread1, thread2]) - self.assert_no_unexpected_exceptions() - self.assert_expected_positions() - - def test_promote_and_promote_to_top_packaged_workbaskets(self): - """Promotes to the top the last-placed packaged workbasket while - promoting the one above it.""" - thread1 = threading.Thread( - target=self.synchronised_call, - kwargs={ - "method_name": "promote_to_top_position", - "packaged_workbasket": self.packaged_workbaskets[4], - }, - name="PromoteToTopThread1", - ) - thread2 = threading.Thread( - target=self.synchronised_call, - kwargs={ - "method_name": "begin_processing", - "packaged_workbasket": self.packaged_workbaskets[3], - }, - name="BeginProcessingThread2", - ) - - self.execute_threads(threads=[thread1, thread2]) - self.assert_no_unexpected_exceptions() - self.assert_expected_positions() - - def test_demote_and_promote_packaged_workbaskets(self): - """Demotes and promotes the same packaged workbasket.""" - thread1 = threading.Thread( - target=self.synchronised_call, - kwargs={ - "method_name": "demote_position", - "packaged_workbasket": self.packaged_workbaskets[2], - }, - name="DemotePositionThread1", - ) - thread2 = threading.Thread( - target=self.synchronised_call, - kwargs={ - "method_name": "promote_position", - "packaged_workbasket": self.packaged_workbaskets[2], - }, - name="PromotePositionThread2", - ) - - self.execute_threads(threads=[thread1, thread2]) - self.assert_no_unexpected_exceptions() - self.assert_expected_positions() - - def test_abandon_and_create_packaged_workbaskets(self): - """Abandons the last-placed packaged workbasket while creating a new - one.""" - thread1 = threading.Thread( - target=self.synchronised_call, - kwargs={ - "method_name": "abandon", - "packaged_workbasket": self.packaged_workbaskets[4], - }, - name="AbandonThread1", - ) - thread2 = threading.Thread( - target=self.synchronised_create_packaged_workbasket, - name="CreateThread2", - ) - - self.execute_threads(threads=[thread1, thread2]) - self.assert_no_unexpected_exceptions() - self.assert_expected_positions() - - def test_crown_dependencies_publishing_pause_and_unpause(unpause_publishing): """Test that Crown Dependencies publishing operational status can be paused and unpaused.""" From eef617b74264aebbe7cb7ecb64473296aebaebf0 Mon Sep 17 00:00:00 2001 From: Dale Cannon <118175145+dalecannon@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:22:37 +0000 Subject: [PATCH 3/4] TP2000-1644 Strengthen Content Security Policy configuration (#1393) * Bump django-csp to 3.8 * Add missing object-src CSP directive * Add missing base-uri CSP directive * Add missing trusted-types CSP directive * Add missing strict-dynamic CSP directive * Create default Trusted Types policy --- common/static/common/js/application.js | 1 + common/static/common/js/trustedTypes.js | 12 ++++++++++++ package-lock.json | 17 +++++++++++++++++ package.json | 3 ++- requirements-dev.txt | 2 +- requirements.txt | 2 +- settings/common.py | 5 +++++ webpack.config.js | 3 +++ 8 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 common/static/common/js/trustedTypes.js diff --git a/common/static/common/js/application.js b/common/static/common/js/application.js index d2fa9f75a..91bc1f7d1 100644 --- a/common/static/common/js/application.js +++ b/common/static/common/js/application.js @@ -9,6 +9,7 @@ const imagePath = (name) => images(name, true); require.context("govuk-frontend/govuk/assets"); +import "./trustedTypes"; import { initAll } from "govuk-frontend"; import showHideCheckboxes from "./showHideCheckboxes"; diff --git a/common/static/common/js/trustedTypes.js b/common/static/common/js/trustedTypes.js new file mode 100644 index 000000000..4fd8cae1d --- /dev/null +++ b/common/static/common/js/trustedTypes.js @@ -0,0 +1,12 @@ +import DOMPurify from "dompurify"; + +/** + * Creates a default Trusted Types policy that serves as a fallback policy + * to sanitise direct sink usage in third-party dependencies. + */ +if (typeof window.trustedTypes !== "undefined") { + window.trustedTypes.createPolicy("default", { + createHTML: (to_escape) => + DOMPurify.sanitize(to_escape, { RETURN_TRUSTED_TYPE: true }), + }); +} diff --git a/package-lock.json b/package-lock.json index 3d7e5a1fd..f27475bd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", "css-loader": "^5.2.6", + "dompurify": "^3.2.3", "file-loader": "^6.2.0", "govuk-frontend": "^3.15.0", "govuk-react": "^0.10.6", @@ -4160,6 +4161,13 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", @@ -6297,6 +6305,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz", + "integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", diff --git a/package.json b/package.json index ae163f067..521a26a5d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", "css-loader": "^5.2.6", + "dompurify": "^3.2.3", "file-loader": "^6.2.0", "govuk-frontend": "^3.15.0", "govuk-react": "^0.10.6", @@ -78,4 +79,4 @@ "pre-commit": "^1.2.2", "react-test-renderer": "^18.2.0" } -} \ No newline at end of file +} diff --git a/requirements-dev.txt b/requirements-dev.txt index f5448caa1..c09a7734e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -r requirements.txt -django_debug_toolbar +django_debug_toolbar==5.0.1 pre-commit diff --git a/requirements.txt b/requirements.txt index 562305540..5cabe8fe8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ defusedxml==0.7.* dj-database-url==0.5.0 django-chunk-upload-handlers==0.0.13 django-crispy-forms==1.14.0 -django-csp==3.6 +django-csp==3.8 django-cte==1.3.1 django-extensions==3.2.3 django-filter==23.5 diff --git a/settings/common.py b/settings/common.py index 1b785b30a..00f385d22 100644 --- a/settings/common.py +++ b/settings/common.py @@ -223,6 +223,7 @@ "https://tagmanager.google.com/", ) CSP_SCRIPT_SRC = ( + "'strict-dynamic'", "'self'", "'unsafe-eval'", "'unsafe-inline'", @@ -231,6 +232,10 @@ "ajax.googleapis.com/", ) CSP_FONT_SRC = ("'self'", "'unsafe-inline'") +CSP_OBJECT_SRC = ("'none'",) +CSP_BASE_URI = ("'none'",) +CSP_REQUIRE_TRUSTED_TYPES_FOR = ("'script'",) +CSP_TRUSTED_TYPES = ("tap#webpack", "dompurify", "default") CSP_INCLUDE_NONCE_IN = ("script-src",) CSP_REPORT_ONLY = False diff --git a/webpack.config.js b/webpack.config.js index 5c98a53be..3198e4648 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,6 +18,9 @@ module.exports = { // (they are picked up by `collectstatic`) publicPath: "/assets/webpack_bundles/", filename: "[name]-[hash].js", + trustedTypes: { + policyName: "tap#webpack", + }, }, plugins: [ From 9030deb0c1f69660d869b16a50ea516e84f8f3af Mon Sep 17 00:00:00 2001 From: Luisella Strona <36708790+Luisella21@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:29:49 +0000 Subject: [PATCH 4/4] TP2000 1650 commodities snapshot behaviour (#1379) Using CommodityCollectionLoader.load(effective_only=True) gives an error, because a filter is applied to a non existing field. Added a small test showing the issue, and changed the code to fix it. Requires migrations: No Requires dependency updates: No --- .gitignore | 1 + commodities/models/dc.py | 3 +-- commodities/tests/test_commodity_tree_snapshot.py | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 1f77e9806..4a88e310e 100644 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,7 @@ ENV/ env.bak/ docker-compose.override.yml settings/envs/docker.override.env +.envrc # Spyder project settings .spyderproject diff --git a/commodities/models/dc.py b/commodities/models/dc.py index f868981ff..e80f0664b 100644 --- a/commodities/models/dc.py +++ b/commodities/models/dc.py @@ -1600,8 +1600,7 @@ def _apply_filters(qs: TrackedModelQuerySet): goods_sids = Subquery(goods_query.values("sid")) indents_query = ( - _apply_filters(GoodsNomenclatureIndent.objects) - .with_end_date() + _apply_filters(GoodsNomenclatureIndent.objects.with_end_date()) .filter(indented_goods_nomenclature__sid__in=goods_sids) .annotate(goods_sid=F("indented_goods_nomenclature__sid")) .order_by("transaction", "validity_start") diff --git a/commodities/tests/test_commodity_tree_snapshot.py b/commodities/tests/test_commodity_tree_snapshot.py index 1781bcca8..fa183b7f1 100644 --- a/commodities/tests/test_commodity_tree_snapshot.py +++ b/commodities/tests/test_commodity_tree_snapshot.py @@ -92,3 +92,11 @@ def test_get_dependent_measures_works_with_wonky_archived_measure( assert wonky_archived_measure.generating_regulation == old_regulation assert target_commodity in commodities_collection.commodities assert target_commodity in target.commodities + + +def test_commodity_collection_loader(seed_database_with_indented_goods): + # Test that 'effective_only' does not crash the code + commodities_collection = CommodityCollectionLoader(prefix="2903").load( + effective_only=True, + ) + assert len(commodities_collection.commodities) == 6