diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 9a3e7f0391f3b1..adbfdd7084e1ad 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -15,7 +15,7 @@ remote_subscriptions: 0003_drop_remote_subscription replays: 0004_index_together -sentry: 0836_create_groupsearchviewstarred_table +sentry: 0837_create_groupsearchviewlastseen_table social_auth: 0002_default_auto_field diff --git a/src/sentry/backup/comparators.py b/src/sentry/backup/comparators.py index e2a81d13ee0811..970ec939562889 100644 --- a/src/sentry/backup/comparators.py +++ b/src/sentry/backup/comparators.py @@ -806,6 +806,9 @@ def get_default_comparators() -> dict[str, list[JSONScrubbingComparator]]: DateUpdatedComparator("date_added", "date_updated"), ], "sentry.groupsearchview": [DateUpdatedComparator("date_updated")], + "sentry.groupsearchviewlastvisited": [ + DateUpdatedComparator("last_visited", "date_added", "date_updated") + ], "sentry.groupsearchviewstarred": [DateUpdatedComparator("date_updated", "date_added")], "sentry.groupsearchviewproject": [ DateUpdatedComparator("date_updated"), diff --git a/src/sentry/migrations/0837_create_groupsearchviewlastseen_table.py b/src/sentry/migrations/0837_create_groupsearchviewlastseen_table.py new file mode 100644 index 00000000000000..d3af2fc742d850 --- /dev/null +++ b/src/sentry/migrations/0837_create_groupsearchviewlastseen_table.py @@ -0,0 +1,74 @@ +# Generated by Django 5.1.5 on 2025-03-04 00:01 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + +import sentry.db.models.fields.bounded +import sentry.db.models.fields.foreignkey +import sentry.db.models.fields.hybrid_cloud_foreign_key +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("sentry", "0836_create_groupsearchviewstarred_table"), + ] + + operations = [ + migrations.CreateModel( + name="GroupSearchViewLastVisited", + fields=[ + ( + "id", + sentry.db.models.fields.bounded.BoundedBigAutoField( + primary_key=True, serialize=False + ), + ), + ("date_updated", models.DateTimeField(auto_now=True)), + ("date_added", models.DateTimeField(auto_now_add=True)), + ( + "user_id", + sentry.db.models.fields.hybrid_cloud_foreign_key.HybridCloudForeignKey( + "sentry.User", db_index=True, on_delete="CASCADE" + ), + ), + ("last_visited", models.DateTimeField(default=django.utils.timezone.now)), + ( + "group_search_view", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sentry.groupsearchview" + ), + ), + ( + "organization", + sentry.db.models.fields.foreignkey.FlexibleForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sentry.organization" + ), + ), + ], + options={ + "db_table": "sentry_groupsearchviewlastvisited", + "constraints": [ + models.UniqueConstraint( + fields=("user_id", "organization_id", "group_search_view_id"), + name="sentry_groupsearchviewlastvisited_unique_last_visited_per_org_user_view", + ) + ], + }, + ), + ] diff --git a/src/sentry/models/__init__.py b/src/sentry/models/__init__.py index 4505f86fa51c97..d31cc910e06f71 100644 --- a/src/sentry/models/__init__.py +++ b/src/sentry/models/__init__.py @@ -53,6 +53,7 @@ from .groupresolution import * # NOQA from .grouprulestatus import * # NOQA from .groupsearchview import * # NOQA +from .groupsearchviewlastvisited import * # NOQA from .groupsearchviewstarred import * # NOQA from .groupseen import * # NOQA from .groupshare import * # NOQA diff --git a/src/sentry/models/groupsearchviewlastvisited.py b/src/sentry/models/groupsearchviewlastvisited.py new file mode 100644 index 00000000000000..f33c1059557438 --- /dev/null +++ b/src/sentry/models/groupsearchviewlastvisited.py @@ -0,0 +1,29 @@ +from django.db import models +from django.db.models import UniqueConstraint +from django.utils import timezone + +from sentry.backup.scopes import RelocationScope +from sentry.db.models import FlexibleForeignKey, region_silo_model +from sentry.db.models.base import DefaultFieldsModel +from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey + + +@region_silo_model +class GroupSearchViewLastVisited(DefaultFieldsModel): + __relocation_scope__ = RelocationScope.Organization + + user_id = HybridCloudForeignKey("sentry.User", on_delete="CASCADE") + organization = FlexibleForeignKey("sentry.Organization") + group_search_view = FlexibleForeignKey("sentry.GroupSearchView") + + last_visited = models.DateTimeField(null=False, default=timezone.now) + + class Meta: + app_label = "sentry" + db_table = "sentry_groupsearchviewlastvisited" + constraints = [ + UniqueConstraint( + fields=["user_id", "organization_id", "group_search_view_id"], + name="sentry_groupsearchviewlastvisited_unique_last_visited_per_org_user_view", + ) + ] diff --git a/src/sentry/organizations/services/organization/impl.py b/src/sentry/organizations/services/organization/impl.py index 00446a425a9f95..073e468c5f2c80 100644 --- a/src/sentry/organizations/services/organization/impl.py +++ b/src/sentry/organizations/services/organization/impl.py @@ -24,6 +24,7 @@ from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView +from sentry.models.groupsearchviewlastvisited import GroupSearchViewLastVisited from sentry.models.groupsearchviewstarred import GroupSearchViewStarred from sentry.models.groupseen import GroupSeen from sentry.models.groupshare import GroupShare @@ -592,6 +593,7 @@ def merge_users(self, *, organization_id: int, from_user_id: int, to_user_id: in GroupSeen, GroupShare, GroupSearchView, + GroupSearchViewLastVisited, GroupSearchViewStarred, GroupSubscription, IncidentActivity, diff --git a/src/sentry/testutils/helpers/backups.py b/src/sentry/testutils/helpers/backups.py index ced626790e5271..2231f5f85d2f26 100644 --- a/src/sentry/testutils/helpers/backups.py +++ b/src/sentry/testutils/helpers/backups.py @@ -74,6 +74,7 @@ from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView, GroupSearchViewProject +from sentry.models.groupsearchviewlastvisited import GroupSearchViewLastVisited from sentry.models.groupsearchviewstarred import GroupSearchViewStarred from sentry.models.groupseen import GroupSeen from sentry.models.groupshare import GroupShare @@ -624,6 +625,12 @@ def create_exhaustive_organization( group_search_view=group_search_view, project=project, ) + GroupSearchViewLastVisited.objects.create( + organization=org, + user_id=owner_id, + group_search_view=group_search_view, + last_visited=timezone.now(), + ) GroupSearchViewStarred.objects.create( organization=org, user_id=owner_id, diff --git a/tests/sentry/users/models/test_user.py b/tests/sentry/users/models/test_user.py index b25ee251f4d853..14244c17693231 100644 --- a/tests/sentry/users/models/test_user.py +++ b/tests/sentry/users/models/test_user.py @@ -15,6 +15,7 @@ from sentry.models.groupassignee import GroupAssignee from sentry.models.groupbookmark import GroupBookmark from sentry.models.groupsearchview import GroupSearchView +from sentry.models.groupsearchviewlastvisited import GroupSearchViewLastVisited from sentry.models.groupsearchviewstarred import GroupSearchViewStarred from sentry.models.groupseen import GroupSeen from sentry.models.groupshare import GroupShare @@ -332,6 +333,7 @@ def test_duplicate_memberships(self, expected_models: list[type[Model]]): GroupSeen, GroupShare, GroupSearchView, + GroupSearchViewLastVisited, GroupSearchViewStarred, GroupSubscription, IncidentActivity, @@ -373,6 +375,7 @@ def test_only_source_user_is_member_of_organization(self, expected_models: list[ GroupSeen, GroupShare, GroupSearchView, + GroupSearchViewLastVisited, GroupSearchViewStarred, GroupSubscription, IncidentActivity,