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:
parent
4208a17847
commit
5b268df29c
@ -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__
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -146,7 +146,7 @@
|
||||
))
|
||||
',
|
||||
<class 'dict'> {
|
||||
'global_cohort_id_0': 38,
|
||||
'global_cohort_id_0': 46,
|
||||
'global_version_0': None,
|
||||
},
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
37
ee/migrations/0015_add_verified_properties.py
Normal file
37
ee/migrations/0015_add_verified_properties.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
@ -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(
|
||||
|
16
ee/models/test/test_property_definition_model.py
Normal file
16
ee/models/test/test_property_definition_model.py
Normal 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
@ -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()
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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" />
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)}
|
||||
|
Loading…
Reference in New Issue
Block a user