diff --git a/api/audit/migrations/0013_allow_manual_override_of_created_date.py b/api/audit/migrations/0013_allow_manual_override_of_created_date.py new file mode 100644 index 000000000000..2bb32c31b617 --- /dev/null +++ b/api/audit/migrations/0013_allow_manual_override_of_created_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2023-12-01 10:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audit', '0012_auto_20230517_1006'), + ] + + operations = [ + migrations.AlterField( + model_name='auditlog', + name='created_date', + field=models.DateTimeField(verbose_name='DateCreated'), + ), + ] diff --git a/api/audit/models.py b/api/audit/models.py index c70b9a296d26..0aaa68bb5638 100644 --- a/api/audit/models.py +++ b/api/audit/models.py @@ -3,6 +3,7 @@ from django.db import models from django.db.models import Model, Q +from django.utils import timezone from django_lifecycle import ( AFTER_CREATE, BEFORE_CREATE, @@ -19,7 +20,7 @@ class AuditLog(LifecycleModel): - created_date = models.DateTimeField("DateCreated", auto_now_add=True) + created_date = models.DateTimeField("DateCreated") project = models.ForeignKey( Project, related_name="audit_logs", null=True, on_delete=models.DO_NOTHING @@ -103,6 +104,11 @@ def add_project(self): if self.environment and self.project is None: self.project = self.environment.project + @hook(BEFORE_CREATE) + def add_created_date(self) -> None: + if not self.created_date: + self.created_date = timezone.now() + @hook( AFTER_CREATE, priority=priority.HIGHEST_PRIORITY, diff --git a/api/audit/tasks.py b/api/audit/tasks.py index 5f03aa275a24..4fbd614df579 100644 --- a/api/audit/tasks.py +++ b/api/audit/tasks.py @@ -1,7 +1,9 @@ import logging import typing +from datetime import datetime from django.contrib.auth import get_user_model +from django.utils import timezone from audit.constants import ( FEATURE_STATE_UPDATED_BY_CHANGE_REQUEST_MESSAGE, @@ -55,6 +57,7 @@ def _create_feature_state_audit_log_for_change_request( project=feature_state.environment.project, log=log, is_system_event=True, + created_date=feature_state.live_from, ) @@ -113,6 +116,7 @@ def create_audit_log_from_historical_record( related_object_type=related_object_type.name, log=log_message, master_api_key=history_instance.master_api_key, + created_date=history_instance.history_date, **instance.get_extra_audit_log_kwargs(history_instance), ) @@ -123,6 +127,7 @@ def create_segment_priorities_changed_audit_log( feature_segment_ids: typing.List[int], user_id: int = None, master_api_key_id: int = None, + changed_at: str = None, ): """ This needs to be a separate task called by the view itself. This is because the OrderedModelBase class @@ -166,4 +171,7 @@ def create_segment_priorities_changed_audit_log( related_object_id=feature.id, related_object_type=RelatedObjectType.FEATURE.name, master_api_key_id=master_api_key_id, + created_date=datetime.fromisoformat(changed_at) + if changed_at is not None + else timezone.now(), ) diff --git a/api/edge_api/identities/tasks.py b/api/edge_api/identities/tasks.py index 918352e79e41..6f1ff63ead0c 100644 --- a/api/edge_api/identities/tasks.py +++ b/api/edge_api/identities/tasks.py @@ -1,6 +1,8 @@ import logging import typing +from django.utils import timezone + from audit.models import AuditLog from audit.related_object_type import RelatedObjectType from environments.models import Environment, Webhook @@ -123,6 +125,7 @@ def generate_audit_log_records( related_object_type=RelatedObjectType.EDGE_IDENTITY.name, related_object_uuid=identity_uuid, master_api_key_id=master_api_key_id, + created_date=timezone.now(), ) ) diff --git a/api/features/models.py b/api/features/models.py index a64a85aa1174..024161757f5e 100644 --- a/api/features/models.py +++ b/api/features/models.py @@ -328,6 +328,7 @@ def sort_function(id_priority_pair): "master_api_key_id": request.master_api_key.id if hasattr(request, "master_api_key") else None, + "changed_at": timezone.now().isoformat(), } ) diff --git a/api/tests/unit/audit/test_unit_audit_tasks.py b/api/tests/unit/audit/test_unit_audit_tasks.py index a69dcc955c97..dfac633c8180 100644 --- a/api/tests/unit/audit/test_unit_audit_tasks.py +++ b/api/tests/unit/audit/test_unit_audit_tasks.py @@ -1,3 +1,5 @@ +from django.utils import timezone + from audit.constants import ( FEATURE_STATE_UPDATED_BY_CHANGE_REQUEST_MESSAGE, FEATURE_STATE_WENT_LIVE_MESSAGE, @@ -11,8 +13,9 @@ create_segment_priorities_changed_audit_log, ) from environments.models import Environment -from features.models import FeatureSegment +from features.models import Feature, FeatureSegment, FeatureState from segments.models import Segment +from users.models import FFAdminUser def test_create_audit_log_from_historical_record_does_nothing_if_no_user_or_api_key( @@ -163,7 +166,11 @@ def test_create_audit_log_from_historical_record_creates_audit_log_with_correct_ instance.get_audit_log_related_object_type.return_value = related_object_type instance.get_extra_audit_log_kwargs.return_value = {} history_instance = mocker.MagicMock( - history_id=1, instance=instance, master_api_key=None, history_type="+" + history_id=1, + instance=instance, + master_api_key=None, + history_type="+", + history_date=timezone.now(), ) history_user = mocker.MagicMock() @@ -205,12 +212,16 @@ def test_create_audit_log_from_historical_record_creates_audit_log_with_correct_ related_object_type=related_object_type.name, log=log_message, master_api_key=None, + created_date=history_instance.history_date, ) def test_create_segment_priorities_changed_audit_log( - admin_user, feature_segment, feature, environment -): + admin_user: FFAdminUser, + feature_segment: FeatureSegment, + feature: Feature, + environment: Environment, +) -> None: # Given another_segment = Segment.objects.create( project=environment.project, name="Another Segment" @@ -219,6 +230,8 @@ def test_create_segment_priorities_changed_audit_log( feature=feature, environment=environment, segment=another_segment ) + now = timezone.now() + # When create_segment_priorities_changed_audit_log( previous_id_priority_pairs=[ @@ -227,16 +240,20 @@ def test_create_segment_priorities_changed_audit_log( ], feature_segment_ids=[feature_segment.id, another_feature_segment.id], user_id=admin_user.id, + changed_at=now.isoformat(), ) # Then assert AuditLog.objects.filter( environment=environment, log=f"Segment overrides re-ordered for feature '{feature.name}'.", + created_date=now, ).exists() -def test_create_feature_state_went_live_audit_log(change_request_feature_state): +def test_create_feature_state_went_live_audit_log( + change_request_feature_state: FeatureState, +) -> None: # Given message = FEATURE_STATE_WENT_LIVE_MESSAGE % ( change_request_feature_state.feature.name, @@ -250,15 +267,18 @@ def test_create_feature_state_went_live_audit_log(change_request_feature_state): # Then assert ( AuditLog.objects.filter( - related_object_id=feature_state_id, is_system_event=True, log=message + related_object_id=feature_state_id, + is_system_event=True, + log=message, + created_date=change_request_feature_state.live_from, ).count() == 1 ) def test_create_feature_state_updated_by_change_request_audit_log( - change_request_feature_state, -): + change_request_feature_state: FeatureState, +) -> None: # Given message = FEATURE_STATE_UPDATED_BY_CHANGE_REQUEST_MESSAGE % ( change_request_feature_state.feature.name, @@ -272,7 +292,10 @@ def test_create_feature_state_updated_by_change_request_audit_log( # Then assert ( AuditLog.objects.filter( - related_object_id=feature_state_id, is_system_event=True, log=message + related_object_id=feature_state_id, + is_system_event=True, + log=message, + created_date=change_request_feature_state.live_from, ).count() == 1 ) diff --git a/api/tests/unit/features/test_unit_features_models.py b/api/tests/unit/features/test_unit_features_models.py index c24db54c6526..0a586f61d0b7 100644 --- a/api/tests/unit/features/test_unit_features_models.py +++ b/api/tests/unit/features/test_unit_features_models.py @@ -1,6 +1,7 @@ from datetime import timedelta from unittest import mock +import freezegun import pytest from django.core.exceptions import ValidationError from django.db.utils import IntegrityError @@ -848,9 +849,10 @@ def test_feature_segment_update_priorities_when_changes( new_id_priority_pairs = [(feature_segment.id, 1), (another_feature_segment.id, 0)] # When - returned_feature_segments = FeatureSegment.update_priorities( - new_feature_segment_id_priorities=new_id_priority_pairs - ) + with freezegun.freeze_time(now): + returned_feature_segments = FeatureSegment.update_priorities( + new_feature_segment_id_priorities=new_id_priority_pairs + ) # Then assert sorted( @@ -864,6 +866,7 @@ def test_feature_segment_update_priorities_when_changes( "feature_segment_ids": [feature_segment.id, another_feature_segment.id], "user_id": mocked_request.user.id, "master_api_key_id": mocked_request.master_api_key.id, + "changed_at": now.isoformat(), } )