0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-21 13:39:22 +01:00

feat: allow verified property definitions (#15937)

Today it annoyed me I could verify an event but not a property

Changes
Adds property definition verification, which was mostly copy-pasta from existing code
This commit is contained in:
Paul D'Ambra 2023-06-08 11:52:25 +01:00 committed by GitHub
parent 4208a17847
commit 5b268df29c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 454 additions and 12416 deletions

View File

@ -1,5 +1,5 @@
from rest_framework import serializers
from django.utils import timezone
from ee.models.property_definition import EnterprisePropertyDefinition
from posthog.api.shared import UserBasicSerializer
from posthog.api.tagged_item import TaggedItemSerializerMixin
@ -9,6 +9,7 @@ from posthog.models.activity_logging.activity_log import dict_changes_between, l
class EnterprisePropertyDefinitionSerializer(TaggedItemSerializerMixin, serializers.ModelSerializer):
updated_by = UserBasicSerializer(read_only=True)
verified_by = UserBasicSerializer(read_only=True)
class Meta:
model = EnterprisePropertyDefinition
@ -23,8 +24,19 @@ class EnterprisePropertyDefinitionSerializer(TaggedItemSerializerMixin, serializ
"query_usage_30_day",
"is_seen_on_filtered_events",
"property_type",
"verified",
"verified_at",
"verified_by",
)
read_only_fields = ["id", "name", "is_numerical", "query_usage_30_day", "is_seen_on_filtered_events"]
read_only_fields = [
"id",
"name",
"is_numerical",
"query_usage_30_day",
"is_seen_on_filtered_events",
"verified_at",
"verified_by",
]
def update(self, property_definition: EnterprisePropertyDefinition, validated_data):
validated_data["updated_by"] = self.context["request"].user
@ -34,6 +46,21 @@ class EnterprisePropertyDefinitionSerializer(TaggedItemSerializerMixin, serializ
else:
validated_data["is_numerical"] = False
if "verified" in validated_data:
if validated_data["verified"] and not property_definition.verified:
# Verify property only if previously unverified
validated_data["verified_by"] = self.context["request"].user
validated_data["verified_at"] = timezone.now()
validated_data["verified"] = True
elif not validated_data["verified"]:
# Unverifying property nullifies verified properties
validated_data["verified_by"] = None
validated_data["verified_at"] = None
validated_data["verified"] = False
else:
# Attempting to re-verify an already verified property, invalid action. Ignore attribute.
validated_data.pop("verified")
before_state = {
k: property_definition.__dict__[k] for k in validated_data.keys() if k in property_definition.__dict__
}

View File

@ -1,4 +1,5 @@
from typing import cast, Optional
from datetime import datetime
from typing import cast, Optional, List, Dict, Any
import dateutil.parser
from django.utils import timezone
@ -7,18 +8,98 @@ from rest_framework import status
from ee.models.event_definition import EnterpriseEventDefinition
from ee.models.license import License, LicenseManager
from posthog.models import Tag, ActivityLog
from posthog.api.test.test_event_definition import capture_event, EventData
from posthog.api.test.test_team import create_team
from posthog.api.test.test_user import create_user
from posthog.models import Tag, ActivityLog, Team, User
from posthog.models.event_definition import EventDefinition
from posthog.tasks.calculate_event_property_usage import calculate_event_property_usage_for_team
from posthog.test.base import APIBaseTest
from posthog.api.test.test_organization import create_organization
@freeze_time("2020-01-02")
class TestEventDefinitionEnterpriseAPI(APIBaseTest):
demo_team: Team = None # type: ignore
user: User = None # type: ignore
"""
Ignoring the verified field we'd expect ordering purchase, watched_movie, entered_free_trial, $pageview
With it we expect watched_movie, entered_free_trial, purchase, $pageview
"""
EXPECTED_EVENT_DEFINITIONS: List[Dict[str, Any]] = [
{"name": "purchase", "volume_30_day": 30, "verified": None},
{"name": "entered_free_trial", "volume_30_day": 15, "verified": True},
{"name": "watched_movie", "volume_30_day": 25, "verified": True},
{"name": "$pageview", "volume_30_day": 14, "verified": None},
]
@classmethod
def setUpTestData(cls):
cls.organization = create_organization(name="test org")
cls.demo_team = create_team(organization=cls.organization)
cls.user = create_user("user", "pass", cls.organization)
for event_definition in cls.EXPECTED_EVENT_DEFINITIONS:
EnterpriseEventDefinition.objects.create(name=event_definition["name"], team_id=cls.demo_team.pk)
for _ in range(event_definition["volume_30_day"]):
capture_event(
event=EventData(
event=event_definition["name"],
team_id=cls.demo_team.pk,
distinct_id="abc",
timestamp=datetime(2020, 1, 1),
properties={},
)
)
# To ensure `volume_30_day` and `query_usage_30_day` are returned non
# None, we need to call this task to have them calculated.
calculate_event_property_usage_for_team(cls.demo_team.pk)
def test_list_event_definitions(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
response = self.client.get("/api/projects/@current/event_definitions/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["count"], len(self.EXPECTED_EVENT_DEFINITIONS))
self.assertEqual(
[(r["name"], r["volume_30_day"], r["verified"]) for r in response.json()["results"]],
[
("purchase", 30, False),
("watched_movie", 25, False),
("entered_free_trial", 15, False),
("$pageview", 14, False),
],
)
for event_definition in self.EXPECTED_EVENT_DEFINITIONS:
definition = EnterpriseEventDefinition.objects.filter(
name=event_definition["name"], team=self.demo_team
).first()
if definition is None:
raise AssertionError(f"Event definition {event_definition['name']} not found")
definition.verified = event_definition["verified"] or False
definition.save()
response = self.client.get("/api/projects/@current/event_definitions/")
assert [(r["name"], r["volume_30_day"], r["verified"]) for r in response.json()["results"]] == [
("watched_movie", 25, True),
("entered_free_trial", 15, True),
("purchase", 30, False),
("$pageview", 14, False),
]
def test_retrieve_existing_event_definition(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event", owner=self.user)
tag = Tag.objects.create(name="deprecated", team_id=self.team.id)
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event", owner=self.user)
tag = Tag.objects.create(name="deprecated", team_id=self.demo_team.id)
event.tagged_items.create(tag_id=tag.id)
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -37,10 +118,10 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EventDefinition.objects.create(team=self.team, name="event")
event = EventDefinition.objects.create(team=self.demo_team, name="event")
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
enterprise_event = EnterpriseEventDefinition.objects.all().first()
enterprise_event = EnterpriseEventDefinition.objects.filter(id=event.id).first()
event.refresh_from_db()
self.assertEqual(enterprise_event.eventdefinition_ptr_id, event.id) # type: ignore
self.assertEqual(enterprise_event.name, event.name) # type: ignore
@ -51,22 +132,26 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
enterprise_property = EnterpriseEventDefinition.objects.create(
team=self.team, name="enterprise event", owner=self.user
team=self.demo_team, name="enterprise event", owner=self.user
)
tag = Tag.objects.create(name="deprecated", team_id=self.team.id)
tag = Tag.objects.create(name="deprecated", team_id=self.demo_team.id)
enterprise_property.tagged_items.create(tag_id=tag.id)
regular_event = EnterpriseEventDefinition.objects.create(team=self.team, name="regular event", owner=self.user)
regular_event = EnterpriseEventDefinition.objects.create(
team=self.demo_team, name="regular event", owner=self.user
)
regular_event.tagged_items.create(tag_id=tag.id)
response = self.client.get(f"/api/projects/@current/event_definitions/?search=enter")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(len(response_data["results"]), 1)
self.assertEqual(
sorted([r["name"] for r in response_data["results"]]), ["entered_free_trial", "enterprise event"]
)
self.assertEqual(response_data["results"][0]["name"], "enterprise event")
self.assertEqual(response_data["results"][0]["description"], "")
self.assertEqual(response_data["results"][0]["tags"], ["deprecated"])
self.assertEqual(response_data["results"][0]["owner"]["id"], self.user.id)
self.assertEqual(response_data["results"][1]["name"], "enterprise event")
self.assertEqual(response_data["results"][1]["description"], "")
self.assertEqual(response_data["results"][1]["tags"], ["deprecated"])
self.assertEqual(response_data["results"][1]["owner"]["id"], self.user.id)
response = self.client.get(f"/api/projects/@current/event_definitions/?search=enterprise")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -76,7 +161,9 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
response = self.client.get(f"/api/projects/@current/event_definitions/?search=e ev")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_data = response.json()
self.assertEqual(len(response_data["results"]), 2)
self.assertEqual(
sorted([r["name"] for r in response_data["results"]]), ["$pageview", "enterprise event", "regular event"]
)
response = self.client.get(f"/api/projects/@current/event_definitions/?search=bust")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -87,7 +174,7 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2038, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event", owner=self.user)
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event", owner=self.user)
response = self.client.patch(
f"/api/projects/@current/event_definitions/{str(event.id)}/",
{"description": "This is a description.", "tags": ["official", "internal"]},
@ -125,7 +212,7 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
]
def test_update_event_without_license(self):
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event")
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event")
response = self.client.patch(
f"/api/projects/@current/event_definitions/{str(event.id)}", data={"description": "test"}
)
@ -136,37 +223,35 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2010, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="description test")
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="description test")
response = self.client.patch(
f"/api/projects/@current/event_definitions/{str(event.id)}", data={"description": "test"}
)
self.assertEqual(response.status_code, status.HTTP_402_PAYMENT_REQUIRED)
self.assertIn("This feature is part of the premium PostHog offering.", response.json()["detail"])
@freeze_time("2021-08-25T22:09:14.252Z")
def test_can_get_event_verification_data(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event", owner=self.user)
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event", owner=self.user)
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
assert response.json()["updated_at"] == "2021-08-25T22:09:14.252000Z"
assert response.json()["updated_at"] == "2020-01-02T00:00:00Z"
query_list_response = self.client.get(f"/api/projects/@current/event_definitions")
matches = [p["name"] for p in query_list_response.json()["results"] if p["name"] == "enterprise event"]
assert len(matches) == 1
@freeze_time("2021-08-25T22:09:14.252Z")
def test_verify_then_unverify(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event", owner=self.user)
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event", owner=self.user)
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -181,7 +266,7 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
assert response.json()["verified"] is True
assert response.json()["verified_by"]["id"] == self.user.id
assert response.json()["verified_at"] == "2021-08-25T22:09:14.252000Z"
assert response.json()["verified_at"] == "2020-01-02T00:00:00Z"
# Unverify the event
self.client.patch(f"/api/projects/@current/event_definitions/{event.id}", {"verified": False})
@ -196,41 +281,43 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event", owner=self.user)
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event", owner=self.user)
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert self.user.team.pk == self.demo_team.pk
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
with freeze_time("2021-08-25T22:09:14.252Z"):
patch_result = self.client.patch(f"/api/projects/@current/event_definitions/{event.id}", {"verified": True})
self.assertEqual(patch_result.status_code, status.HTTP_200_OK, patch_result.json())
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK, response.json())
assert response.json()["verified"] is True
assert response.json()["verified_by"]["id"] == self.user.id
assert response.json()["verified_at"] == "2020-01-02T00:00:00Z"
assert response.json()["updated_at"] == "2020-01-02T00:00:00Z"
with freeze_time("2020-01-02T00:01:00Z"):
self.client.patch(f"/api/projects/@current/event_definitions/{event.id}", {"verified": True})
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is True
assert response.json()["verified_by"]["id"] == self.user.id
assert response.json()["verified_at"] == "2021-08-25T22:09:14.252000Z"
assert response.json()["updated_at"] == "2021-08-25T22:09:14.252000Z"
with freeze_time("2021-10-26T22:09:14.252Z"):
self.client.patch(f"/api/projects/@current/event_definitions/{event.id}", {"verified": True})
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is True
assert response.json()["verified_by"]["id"] == self.user.id
assert response.json()["verified_at"] == "2021-08-25T22:09:14.252000Z" # Note `verified_at` did not change
assert response.json()["verified_at"] == "2020-01-02T00:00:00Z" # Note `verified_at` did not change
# updated_at automatically updates on every patch request
assert response.json()["updated_at"] == "2021-10-26T22:09:14.252000Z"
assert response.json()["updated_at"] == "2020-01-02T00:01:00Z"
@freeze_time("2021-08-25T22:09:14.252Z")
def test_cannot_update_verified_meta_properties_directly(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event", owner=self.user)
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event", owner=self.user)
response = self.client.get(f"/api/projects/@current/event_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -238,7 +325,7 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
with freeze_time("2021-08-25T22:09:14.252Z"):
with freeze_time("2020-01-02T00:01:00Z"):
self.client.patch(
f"/api/projects/@current/event_definitions/{event.id}",
{
@ -259,7 +346,7 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
key="key_123", plan="enterprise", valid_until=timezone.datetime(2038, 1, 19, 3, 14, 7)
)
event = EnterpriseEventDefinition.objects.create(team=self.team, name="enterprise event")
event = EnterpriseEventDefinition.objects.create(team=self.demo_team, name="enterprise event")
response = self.client.patch(
f"/api/projects/@current/event_definitions/{str(event.id)}", data={"tags": ["a", "b", "a"]}
)
@ -270,8 +357,8 @@ class TestEventDefinitionEnterpriseAPI(APIBaseTest):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
EnterpriseEventDefinition.objects.create(team=self.team, name="rated_app")
EnterpriseEventDefinition.objects.create(team=self.team, name="installed_app")
EnterpriseEventDefinition.objects.create(team=self.demo_team, name="rated_app")
EnterpriseEventDefinition.objects.create(team=self.demo_team, name="installed_app")
response = self.client.get("/api/projects/@current/event_definitions/?search=app&event_type=event")
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1,5 +1,5 @@
from typing import cast, Optional
from typing import cast, Optional, List, Dict
from freezegun import freeze_time
import pytest
from django.db.utils import IntegrityError
from django.utils import timezone
@ -279,3 +279,163 @@ class TestPropertyDefinitionEnterpriseAPI(APIBaseTest):
)
self.assertListEqual(sorted(response.json()["tags"]), ["a", "b"])
@freeze_time("2021-08-25T22:09:14.252Z")
def test_can_get_property_verification_data(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterprisePropertyDefinition.objects.create(team=self.team, name="enterprise property")
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
assert response.json()["updated_at"] == "2021-08-25T22:09:14.252000Z"
query_list_response = self.client.get(f"/api/projects/@current/property_definitions")
matches = [p["name"] for p in query_list_response.json()["results"] if p["name"] == "enterprise property"]
assert len(matches) == 1
@freeze_time("2021-08-25T22:09:14.252Z")
def test_verify_then_unverify(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterprisePropertyDefinition.objects.create(team=self.team, name="enterprise property")
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
# Verify the event
self.client.patch(f"/api/projects/@current/property_definitions/{event.id}", {"verified": True})
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is True
assert response.json()["verified_by"]["id"] == self.user.id
assert response.json()["verified_at"] == "2021-08-25T22:09:14.252000Z"
# Unverify the event
self.client.patch(f"/api/projects/@current/property_definitions/{event.id}", {"verified": False})
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
def test_verify_then_verify_again_no_change(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterprisePropertyDefinition.objects.create(team=self.team, name="enterprise property")
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
with freeze_time("2021-08-25T22:09:14.252Z"):
self.client.patch(f"/api/projects/@current/property_definitions/{event.id}", {"verified": True})
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is True
assert response.json()["verified_by"]["id"] == self.user.id
assert response.json()["verified_at"] == "2021-08-25T22:09:14.252000Z"
assert response.json()["updated_at"] == "2021-08-25T22:09:14.252000Z"
with freeze_time("2021-10-26T22:09:14.252Z"):
self.client.patch(f"/api/projects/@current/property_definitions/{event.id}", {"verified": True})
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is True
assert response.json()["verified_by"]["id"] == self.user.id
assert response.json()["verified_at"] == "2021-08-25T22:09:14.252000Z" # Note `verified_at` did not change
# updated_at automatically updates on every patch request
assert response.json()["updated_at"] == "2021-10-26T22:09:14.252000Z"
@freeze_time("2021-08-25T22:09:14.252Z")
def test_cannot_update_verified_meta_properties_directly(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
event = EnterprisePropertyDefinition.objects.create(team=self.team, name="enterprise property")
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
with freeze_time("2021-08-25T22:09:14.252Z"):
self.client.patch(
f"/api/projects/@current/property_definitions/{event.id}",
{
"verified_by": self.user.id,
"verified_at": timezone.now(),
}, # These properties are ignored by the serializer
)
response = self.client.get(f"/api/projects/@current/property_definitions/{event.id}")
self.assertEqual(response.status_code, status.HTTP_200_OK)
assert response.json()["verified"] is False
assert response.json()["verified_by"] is None
assert response.json()["verified_at"] is None
def test_list_property_definitions(self):
super(LicenseManager, cast(LicenseManager, License.objects)).create(
plan="enterprise", valid_until=timezone.datetime(2500, 1, 19, 3, 14, 7)
)
properties: List[Dict] = [
{"name": "4_when_verified", "query_usage_30_day": 4, "verified": False},
{"name": "5_when_verified", "query_usage_30_day": 4, "verified": False},
{"name": "1_when_verified", "query_usage_30_day": 4, "verified": True},
{"name": "2_when_verified", "query_usage_30_day": 3, "verified": True},
{"name": "6_when_verified", "query_usage_30_day": 1, "verified": False},
{"name": "3_when_verified", "query_usage_30_day": 1, "verified": True},
]
for property in properties:
EnterprisePropertyDefinition.objects.create(
team=self.team, name=property["name"], query_usage_30_day=property["query_usage_30_day"]
)
response = self.client.get("/api/projects/@current/property_definitions/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["count"], len(properties))
assert [(r["name"], r["query_usage_30_day"], r["verified"]) for r in response.json()["results"]] == [
("1_when_verified", 4, False),
("4_when_verified", 4, False),
("5_when_verified", 4, False),
("2_when_verified", 3, False),
("3_when_verified", 1, False),
("6_when_verified", 1, False),
]
for property in properties:
definition = EnterprisePropertyDefinition.objects.filter(name=property["name"], team=self.team).first()
if definition is None:
raise AssertionError(f"Property definition {property['name']} not found")
definition.verified = property["verified"] or False
definition.save()
response = self.client.get("/api/projects/@current/property_definitions/")
assert [(r["name"], r["query_usage_30_day"], r["verified"]) for r in response.json()["results"]] == [
("1_when_verified", 4, True),
("2_when_verified", 3, True),
("3_when_verified", 1, True),
("4_when_verified", 4, False),
("5_when_verified", 4, False),
("6_when_verified", 1, False),
]

View File

@ -83,7 +83,7 @@
(SELECT pdi.person_id AS person_id,
countIf(timestamp > now() - INTERVAL 2 year
AND timestamp < now()
AND event = '$pageview') > 0 AS performed_event_condition_6_level_level_0_level_0_level_0_0
AND event = '$pageview') > 0 AS performed_event_condition_14_level_level_0_level_0_level_0_0
FROM events e
INNER JOIN
(SELECT distinct_id,
@ -113,7 +113,7 @@
HAVING max(is_deleted) = 0
AND (((((NOT has(['something1'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), '$some_prop'), '^"|"$', ''))))))))) person ON person.person_id = behavior_query.person_id
WHERE 1 = 1
AND ((((performed_event_condition_6_level_level_0_level_0_level_0_0)))) ) as person
AND ((((performed_event_condition_14_level_level_0_level_0_level_0_0)))) ) as person
UNION ALL
SELECT person_id,
cohort_id,
@ -148,7 +148,7 @@
(SELECT pdi.person_id AS person_id,
countIf(timestamp > now() - INTERVAL 2 year
AND timestamp < now()
AND event = '$pageview') > 0 AS performed_event_condition_8_level_level_0_level_0_level_0_0
AND event = '$pageview') > 0 AS performed_event_condition_16_level_level_0_level_0_level_0_0
FROM events e
INNER JOIN
(SELECT distinct_id,
@ -178,7 +178,7 @@
HAVING max(is_deleted) = 0
AND (((((NOT has(['something1'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), '$some_prop'), '^"|"$', ''))))))))) person ON person.person_id = behavior_query.person_id
WHERE 1 = 1
AND ((((performed_event_condition_8_level_level_0_level_0_level_0_0)))) ) ))
AND ((((performed_event_condition_16_level_level_0_level_0_level_0_0)))) ) ))
'
---
# name: TestCohort.test_cohortpeople_with_not_in_cohort_operator_for_behavioural_cohorts
@ -195,7 +195,7 @@
FROM
(SELECT pdi.person_id AS person_id,
minIf(timestamp, event = 'signup') >= now() - INTERVAL 15 day
AND minIf(timestamp, event = 'signup') < now() as first_time_condition_9_level_level_0_level_0_0
AND minIf(timestamp, event = 'signup') < now() as first_time_condition_17_level_level_0_level_0_0
FROM events e
INNER JOIN
(SELECT distinct_id,
@ -208,7 +208,7 @@
AND event IN ['signup']
GROUP BY person_id) behavior_query
WHERE 1 = 1
AND (((first_time_condition_9_level_level_0_level_0_0))) ) as person
AND (((first_time_condition_17_level_level_0_level_0_0))) ) as person
UNION ALL
SELECT person_id,
cohort_id,
@ -237,9 +237,9 @@
(SELECT pdi.person_id AS person_id,
countIf(timestamp > now() - INTERVAL 2 year
AND timestamp < now()
AND event = '$pageview') > 0 AS performed_event_condition_10_level_level_0_level_0_level_0_0,
AND event = '$pageview') > 0 AS performed_event_condition_18_level_level_0_level_0_level_0_0,
minIf(timestamp, event = 'signup') >= now() - INTERVAL 15 day
AND minIf(timestamp, event = 'signup') < now() as first_time_condition_10_level_level_0_level_1_level_0_level_0_level_0_0
AND minIf(timestamp, event = 'signup') < now() as first_time_condition_18_level_level_0_level_1_level_0_level_0_level_0_0
FROM events e
INNER JOIN
(SELECT distinct_id,
@ -252,8 +252,8 @@
AND event IN ['$pageview', 'signup']
GROUP BY person_id) behavior_query
WHERE 1 = 1
AND ((((performed_event_condition_10_level_level_0_level_0_level_0_0))
AND ((((NOT first_time_condition_10_level_level_0_level_1_level_0_level_0_level_0_0)))))) ) as person
AND ((((performed_event_condition_18_level_level_0_level_0_level_0_0))
AND ((((NOT first_time_condition_18_level_level_0_level_1_level_0_level_0_level_0_0)))))) ) as person
UNION ALL
SELECT person_id,
cohort_id,

View File

@ -146,7 +146,7 @@
))
',
<class 'dict'> {
'global_cohort_id_0': 38,
'global_cohort_id_0': 46,
'global_version_0': None,
},
)

View File

@ -1,6 +1,6 @@
# name: ClickhouseTestExperimentSecondaryResults.test_basic_secondary_metric_results
'
/* user_id:51 celery:posthog.celery.sync_insight_caching_state */
/* user_id:55 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events

View File

@ -1,6 +1,6 @@
# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results
'
/* user_id:57 celery:posthog.celery.sync_insight_caching_state */
/* user_id:61 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events
@ -138,7 +138,7 @@
---
# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_and_events_out_of_time_range_timezones
'
/* user_id:58 celery:posthog.celery.sync_insight_caching_state */
/* user_id:62 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events
@ -276,7 +276,7 @@
---
# name: ClickhouseTestFunnelExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants
'
/* user_id:60 celery:posthog.celery.sync_insight_caching_state */
/* user_id:64 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events
@ -414,7 +414,7 @@
---
# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results
'
/* user_id:62 celery:posthog.celery.sync_insight_caching_state */
/* user_id:66 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events
@ -611,7 +611,7 @@
---
# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_for_three_test_variants
'
/* user_id:63 celery:posthog.celery.sync_insight_caching_state */
/* user_id:67 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events
@ -754,7 +754,7 @@
---
# name: ClickhouseTestTrendExperimentResults.test_experiment_flow_with_event_results_out_of_timerange_timezone
'
/* user_id:65 celery:posthog.celery.sync_insight_caching_state */
/* user_id:69 celery:posthog.celery.sync_insight_caching_state */
SELECT team_id,
date_diff('second', max(timestamp), now()) AS age
FROM events

View File

@ -0,0 +1,37 @@
# Generated by Django 3.2.18 on 2023-06-07 10:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("ee", "0014_roles_memberships_and_resource_access"),
]
operations = [
migrations.AddField(
model_name="enterprisepropertydefinition",
name="verified",
field=models.BooleanField(blank=True, default=False),
),
migrations.AddField(
model_name="enterprisepropertydefinition",
name="verified_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="enterprisepropertydefinition",
name="verified_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="property_verifying_user",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -9,6 +9,12 @@ class EnterprisePropertyDefinition(PropertyDefinition):
updated_at: models.DateTimeField = models.DateTimeField(auto_now=True)
updated_by = models.ForeignKey("posthog.User", null=True, on_delete=models.SET_NULL, blank=True)
verified: models.BooleanField = models.BooleanField(default=False, blank=True)
verified_at: models.DateTimeField = models.DateTimeField(null=True, blank=True)
verified_by = models.ForeignKey(
"posthog.User", null=True, on_delete=models.SET_NULL, blank=True, related_name="property_verifying_user"
)
# Deprecated in favour of app-wide tagging model. See EnterpriseTaggedItem
deprecated_tags: ArrayField = ArrayField(models.CharField(max_length=32), null=True, blank=True, default=list)
deprecated_tags_v2: ArrayField = ArrayField(

View File

@ -0,0 +1,16 @@
import pytest
from ee.models.property_definition import EnterprisePropertyDefinition
from posthog.test.base import BaseTest
class TestPropertyDefinition(BaseTest):
def test_errors_on_invalid_verified_by_type(self):
with pytest.raises(ValueError):
EnterprisePropertyDefinition.objects.create(
team=self.team, name="enterprise property", verified_by="Not user id" # type: ignore
)
def test_default_verified_false(self):
definition = EnterprisePropertyDefinition.objects.create(team=self.team, name="enterprise property")
assert definition.verified is False

File diff suppressed because it is too large Load Diff

View File

@ -1,105 +0,0 @@
from django.db import transaction
from django.db.models import Q
from posthog.models import Tag as TagModel
from posthog.models import TaggedItem as TaggedItemModel
from posthog.models import Team
from posthog.test.base import TestMigrations
class TagsTestCase(TestMigrations):
migrate_from = "0011_add_tags_back"
migrate_to = "0012_migrate_tags_v2"
assert_snapshots = True
@property
def app(self):
return "ee"
def setUpBeforeMigration(self, apps):
EnterpriseEventDefinition = apps.get_model("ee", "EnterpriseEventDefinition")
EnterprisePropertyDefinition = apps.get_model("ee", "EnterprisePropertyDefinition")
# Setup
# apps.get_model("posthog", "Tag") doesn't work in setup because of a dependency issue
tag = TagModel.objects.create(name="existing tag", team_id=self.team.id)
self.event_definition = EnterpriseEventDefinition.objects.create(
team_id=self.team.id,
name="enterprise event",
deprecated_tags=["a", "b", "c", "a", "b", "existing tag", "", " ", None],
)
self.property_definition_with_tags = EnterprisePropertyDefinition.objects.create(
team_id=self.team.id, name="property def with tags", deprecated_tags=["c", "d", "d", "existing tag"]
)
self.property_definition_without_tags = EnterprisePropertyDefinition.objects.create(
team_id=self.team.id, name="property def without tags"
)
TaggedItemModel.objects.create(tag=tag, property_definition_id=self.property_definition_with_tags.id)
# Setup for batched tags
self.team2 = Team.objects.create(
organization=self.organization,
api_token="token12345",
test_account_filters=[
{"key": "email", "value": "@posthog.com", "operator": "not_icontains", "type": "person"}
],
)
self.team2_total_property_definitions = 1_001
tag2 = TagModel.objects.create(name="existing tag", team_id=self.team2.id)
with transaction.atomic():
for _tag in range(self.team2_total_property_definitions):
EnterprisePropertyDefinition.objects.create(
name=f"batch_prop_{_tag}", team_id=self.team2.id, deprecated_tags=[_tag, "existing tag"]
)
TaggedItemModel.objects.create(
tag=tag2,
property_definition_id=EnterprisePropertyDefinition.objects.filter(team_id=self.team2.id).first().id,
)
def test_tags_migrated(self):
Tag = self.apps.get_model("posthog", "Tag") # type: ignore
TaggedItem = self.apps.get_model("posthog", "TaggedItem") # type: ignore
EnterpriseEventDefinition = self.apps.get_model("ee", "EnterpriseEventDefinition") # type: ignore
EnterprisePropertyDefinition = self.apps.get_model("ee", "EnterprisePropertyDefinition") # type: ignore
event_definition = EnterpriseEventDefinition.objects.get(id=self.event_definition.id)
self.assertEqual(
list(event_definition.tagged_items.order_by("tag__name").values_list("tag__name", flat=True)),
["a", "b", "c", "existing tag"],
)
property_definition_with_tags = EnterprisePropertyDefinition.objects.get(
id=self.property_definition_with_tags.id
)
self.assertEqual(
list(property_definition_with_tags.tagged_items.order_by("tag__name").values_list("tag__name", flat=True)),
["c", "d", "existing tag"],
)
property_definition_without_tags = EnterprisePropertyDefinition.objects.get(
id=self.property_definition_without_tags.id
)
self.assertEqual(property_definition_without_tags.tagged_items.count(), 0)
self.assertEqual(
sorted(Tag.objects.filter(team_id=self.team.id).all().values_list("name", flat=True)),
["a", "b", "c", "d", "existing tag"],
)
# By the end of the migration, the total count for team 2 should be
# Tags = team2_total_property_definitions + 2 + team1_tags
# TaggedItems = team2_total_property_definitions * 2 + team1_taggeditems
self.assertEqual(Tag.objects.all().count(), self.team2_total_property_definitions + 1 + 5)
self.assertEqual(TaggedItem.objects.all().count(), self.team2_total_property_definitions * 2 + 7)
def tearDown(self):
EnterpriseEventDefinition = self.apps.get_model("ee", "EnterpriseEventDefinition") # type: ignore
EnterprisePropertyDefinition = self.apps.get_model("ee", "EnterprisePropertyDefinition") # type: ignore
EnterprisePropertyDefinition.objects.filter(
Q(id__in=[self.property_definition_with_tags.id, self.property_definition_without_tags.id])
| Q(name__startswith="batch_prop_")
).delete()
EnterpriseEventDefinition.objects.filter(id=self.event_definition.id).delete()
Team.objects.get(id=self.team2.id).delete()
super().tearDown()

View File

@ -90,17 +90,20 @@ function TaxonomyIntroductionSection(): JSX.Element {
)
}
export function VerifiedEventCheckbox({
export function VerifiedDefinitionCheckbox({
verified,
isProperty,
onChange,
compact = false,
}: {
verified: boolean
isProperty: boolean
onChange: (nextVerified: boolean) => void
compact?: boolean
}): JSX.Element {
const copy =
'Verified events are prioritized in filters and other selection components. Verifying an event is a signal to collaborators that this event should be used in favor of similar events.'
const copy = isProperty
? 'Verifying a property is a signal to collaborators that this property should be used in favor of similar properties.'
: 'Verified events are prioritized in filters and other selection components. Verifying an event is a signal to collaborators that this event should be used in favor of similar events.'
return (
<div className="border p-2 rounded">
@ -111,7 +114,7 @@ export function VerifiedEventCheckbox({
}}
>
<span className="font-semibold">
Verified event
Verified {isProperty ? 'property' : 'event'}
{compact && (
<Tooltip title={copy}>
<IconInfo className="ml-1 text-muted text-xl shrink-0" />
@ -317,6 +320,7 @@ function DefinitionEdit(): JSX.Element {
type,
dirty,
viewFullDetailUrl,
isProperty,
} = useValues(definitionPopoverLogic)
const { setLocalDefinition, handleCancel, handleSave } = useActions(definitionPopoverLogic)
@ -364,8 +368,9 @@ function DefinitionEdit(): JSX.Element {
</>
)}
{definition && definition.name && !isPostHogProp(definition.name) && 'verified' in localDefinition && (
<VerifiedEventCheckbox
<VerifiedDefinitionCheckbox
verified={!!localDefinition.verified}
isProperty={isProperty}
onChange={(nextVerified) => {
setLocalDefinition({ verified: nextVerified })
}}
@ -437,7 +442,7 @@ export function ControlledDefinitionPopover({
const icon = group.getIcon?.(definition || item)
// Must use `useEffect` here to hydrate popover card with newest item, since lifecycle of `ItemPopover` is controlled
// Must use `useEffect` here to hydrate popover card with the newest item, since lifecycle of `ItemPopover` is controlled
// independently by `infiniteListLogic`
useEffect(() => {
setDefinition(item)

View File

@ -7,7 +7,7 @@ import { Field } from 'lib/forms/Field'
import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea/LemonTextArea'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { getPropertyLabel, isPostHogProp } from 'lib/components/PropertyKeyInfo'
import { VerifiedEventCheckbox } from 'lib/components/DefinitionPopover/DefinitionPopoverContents'
import { VerifiedDefinitionCheckbox } from 'lib/components/DefinitionPopover/DefinitionPopoverContents'
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
import { Form } from 'kea-forms'
import { tagsModel } from '~/models/tagsModel'
@ -15,14 +15,16 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider'
export function DefinitionEdit(props: DefinitionEditLogicProps): JSX.Element {
const logic = definitionEditLogic(props)
const { definitionLoading, definition, hasTaxonomyFeatures, isEvent, isProperty } = useValues(logic)
const { definitionLoading, definition, hasTaxonomyFeatures, isProperty } = useValues(logic)
const { setPageMode, saveDefinition } = useActions(logic)
const { tags, tagsLoading } = useValues(tagsModel)
const showVerifiedCheckbox = hasTaxonomyFeatures && !isPostHogProp(definition.name) && 'verified' in definition
return (
<Form logic={definitionEditLogic} props={props} formKey="definition">
<PageHeader
title="Edit event"
title={`Edit ${isProperty ? 'Property' : 'Event'} Definition`}
buttons={
<>
<LemonButton
@ -66,11 +68,12 @@ export function DefinitionEdit(props: DefinitionEditLogicProps): JSX.Element {
</Field>
</div>
)}
{hasTaxonomyFeatures && isEvent && !isPostHogProp(definition.name) && 'verified' in definition && (
{showVerifiedCheckbox && (
<div className="mt-4 ph-ignore-input">
<Field name="verified" data-attr="definition-verified">
{({ value, onChange }) => (
<VerifiedEventCheckbox
<VerifiedDefinitionCheckbox
isProperty={isProperty}
verified={!!value}
onChange={(nextVerified) => {
onChange(nextVerified)

View File

@ -29,6 +29,13 @@ export function getPropertyDefinitionIcon(definition: PropertyDefinition): JSX.E
</Tooltip>
)
}
if (!!definition.verified) {
return (
<Tooltip title="Verified event property">
<VerifiedPropertyIcon className="taxonomy-icon taxonomy-icon-verified" />
</Tooltip>
)
}
return (
<Tooltip title="Event property">
<PropertyIcon className="taxonomy-icon taxonomy-icon-muted" />

View File

@ -2293,6 +2293,9 @@ export interface PropertyDefinition {
last_seen_at?: string // TODO: Implement
example?: string
is_action?: boolean
verified?: boolean
verified_at?: string
verified_by?: string
}
export enum PropertyDefinitionState {

View File

@ -2,7 +2,7 @@ admin: 0003_logentry_add_action_flag_choices
auth: 0012_alter_user_first_name_max_length
axes: 0006_remove_accesslog_trusted
contenttypes: 0002_remove_content_type_name
ee: 0014_roles_memberships_and_resource_access
ee: 0015_add_verified_properties
otp_static: 0002_throttling
otp_totp: 0002_auto_20190420_0723
posthog: 0320_survey

View File

@ -87,7 +87,7 @@ class EventDefinitionViewSet(
search_query, search_kwargs = term_search_filter_sql(self.search_fields, search)
params = {"team_id": self.team_id, "is_posthog_event": "$%", **search_kwargs}
order, order_direction = self._ordering_params_from_request()
order_expressions = [self._ordering_params_from_request()]
ingestion_taxonomy_is_available = self.organization.is_feature_available(AvailableFeature.INGESTION_TAXONOMY)
is_enterprise = EE_AVAILABLE and ingestion_taxonomy_is_available
@ -103,6 +103,14 @@ class EventDefinitionViewSet(
to_attr="prefetched_tags",
)
)
order_expressions = (
[
("verified", "DESC"),
("volume_30_day", "DESC"),
]
if order_expressions == [("volume_30_day", "DESC")]
else order_expressions
)
else:
event_definition_object_manager = EventDefinition.objects
@ -110,8 +118,7 @@ class EventDefinitionViewSet(
event_type,
is_enterprise=is_enterprise,
conditions=search_query,
order_expr=order,
order_direction=order_direction,
order_expressions=order_expressions,
)
return event_definition_object_manager.raw(sql, params=params)

View File

@ -221,7 +221,8 @@ class QueryContext:
},
)
def as_sql(self):
def as_sql(self, order_by_verified: bool):
verified_ordering = "verified DESC NULLS LAST," if order_by_verified else ""
query = f"""
SELECT {self.property_definition_fields}, {self.event_property_field} AS is_seen_on_filtered_events
FROM {self.table}
@ -232,7 +233,7 @@ class QueryContext:
{self.excluded_properties_filter}
{self.name_filter} {self.numerical_filter} {self.search_query} {self.event_property_filter} {self.is_feature_flag_filter}
{self.event_name_filter}
ORDER BY is_seen_on_filtered_events DESC, posthog_propertydefinition.query_usage_30_day DESC NULLS LAST, posthog_propertydefinition.name ASC
ORDER BY {verified_ordering} is_seen_on_filtered_events DESC, posthog_propertydefinition.query_usage_30_day DESC NULLS LAST, posthog_propertydefinition.name ASC
LIMIT {self.limit} OFFSET {self.offset}
"""
@ -381,6 +382,7 @@ class PropertyDefinitionViewSet(
)
use_enterprise_taxonomy = self.request.user.organization.is_feature_available(AvailableFeature.INGESTION_TAXONOMY) # type: ignore
order_by_verified = False
if use_enterprise_taxonomy:
try:
from ee.models.property_definition import EnterprisePropertyDefinition
@ -399,6 +401,7 @@ class PropertyDefinitionViewSet(
"tagged_items", queryset=TaggedItem.objects.select_related("tag"), to_attr="prefetched_tags"
)
)
order_by_verified = True
except ImportError:
use_enterprise_taxonomy = False
@ -442,7 +445,7 @@ class PropertyDefinitionViewSet(
self.paginator.set_count(full_count) # type: ignore
return queryset.raw(query_context.as_sql(), params=query_context.params)
return queryset.raw(query_context.as_sql(order_by_verified), params=query_context.params)
def get_serializer_class(self) -> Type[serializers.ModelSerializer]:
serializer_class = self.serializer_class

View File

@ -1,7 +1,7 @@
import json
import re
from enum import Enum, auto
from typing import List, Literal, Optional, Union
from typing import List, Literal, Optional, Union, Tuple
from uuid import UUID
import structlog
@ -239,8 +239,7 @@ def create_event_definitions_sql(
event_type: EventDefinitionType,
is_enterprise: bool = False,
conditions: str = "",
order_expr: str = "",
order_direction: Literal["ASC", "DESC"] = "DESC",
order_expressions: List[Tuple[str, Literal["ASC", "DESC"]]] = [],
) -> str:
if is_enterprise:
from ee.models import EnterpriseEventDefinition
@ -266,11 +265,13 @@ def create_event_definitions_sql(
if event_type == EventDefinitionType.EVENT_POSTHOG:
conditions += " AND posthog_eventdefinition.name LIKE %(is_posthog_event)s"
additional_ordering = (
f"{order_expr} {order_direction} NULLS {'FIRST' if order_direction == 'ASC' else 'LAST'}, "
if order_expr
else ""
)
additional_ordering = ""
for order_expression, order_direction in order_expressions:
additional_ordering += (
f"{order_expression} {order_direction} NULLS {'FIRST' if order_direction == 'ASC' else 'LAST'}, "
if order_expression
else ""
)
return f"""
SELECT {",".join(event_definition_fields)}