class EnterpriseEventDefinitionSerializer(TaggedItemSerializerMixin, serializers.ModelSerializer): updated_by = UserBasicSerializer(read_only=True) verified_by = UserBasicSerializer(read_only=True) class Meta: model = EnterpriseEventDefinition fields = ( "id", "name", "owner", "description", "tags", "volume_30_day", "query_usage_30_day", "created_at", "updated_at", "updated_by", "last_seen_at", "verified", "verified_at", "verified_by", ) read_only_fields = [ "id", "name", "created_at", "updated_at", "volume_30_day", "query_usage_30_day", "last_seen_at", "verified_at", "verified_by", ] def update(self, event_definition: EnterpriseEventDefinition, validated_data): validated_data["updated_by"] = self.context["request"].user if "verified" in validated_data: if validated_data["verified"] and not event_definition.verified: # Verify event 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 event 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 event, invalid action. Ignore attribute. validated_data.pop("verified") return super().update(event_definition, validated_data) def to_representation(self, instance): representation = super().to_representation(instance) representation["owner"] = UserBasicSerializer(instance=instance.owner).data if instance.owner else None return representation
class EnterpriseEventDefinitionSerializer(serializers.ModelSerializer): updated_by = UserBasicSerializer(read_only=True) class Meta: model = EnterpriseEventDefinition fields = ( "id", "name", "owner", "description", "tags", "volume_30_day", "query_usage_30_day", "updated_at", "updated_by", ) read_only_fields = [ "id", "name", "updated_at", "volume_30_day", "query_usage_30_day" ] def update(self, event_definition: EnterpriseEventDefinition, validated_data): validated_data["updated_by"] = self.context["request"].user return super().update(event_definition, validated_data) def to_representation(self, instance): representation = super().to_representation(instance) representation["owner"] = UserBasicSerializer( instance=instance.owner).data return representation
class OrganizationMemberSerializer(serializers.ModelSerializer): user = UserBasicSerializer(read_only=True) class Meta: model = OrganizationMembership fields = [ "id", "user", "level", "joined_at", "updated_at", ] read_only_fields = ["id", "joined_at", "updated_at"] def update(self, updated_membership, validated_data, **kwargs): updated_membership = cast(OrganizationMembership, updated_membership) raise_errors_on_nested_writes("update", self, validated_data) requesting_membership: OrganizationMembership = OrganizationMembership.objects.get( organization=updated_membership.organization, user=self.context["request"].user, ) for attr, value in validated_data.items(): if attr == "level": requesting_membership.validate_update(updated_membership, value) setattr(updated_membership, attr, value) updated_membership.save() return updated_membership
class EnterprisePropertyDefinitionSerializer(TaggedItemSerializerMixin, serializers.ModelSerializer): updated_by = UserBasicSerializer(read_only=True) class Meta: model = EnterprisePropertyDefinition fields = ( "id", "name", "description", "tags", "is_numerical", "updated_at", "updated_by", "query_usage_30_day", "is_event_property", "property_type", ) read_only_fields = [ "id", "name", "is_numerical", "query_usage_30_day", "is_event_property" ] def update(self, event_definition: EnterprisePropertyDefinition, validated_data): validated_data["updated_by"] = self.context["request"].user return super().update(event_definition, validated_data)
class SessionsFilterSerializer(serializers.ModelSerializer): created_by = UserBasicSerializer(read_only=True) class Meta: model = SessionsFilter fields = [ "id", "name", "created_by", "created_at", "updated_at", "filters" ] read_only_fields = [ "id", "created_by", "created_at", "updated_at", ] def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> SessionsFilter: request = self.context["request"] instance = SessionsFilter.objects.create( team_id=self.context["team_id"], created_by=request.user, **validated_data, ) posthoganalytics.capture(instance.created_by.distinct_id, "sessions filter created") return instance
class OrganizationMemberSerializer(serializers.ModelSerializer): user_first_name = serializers.CharField(source="user.first_name", read_only=True) user_email = serializers.CharField(source="user.email", read_only=True) membership_id = serializers.CharField(source="id", read_only=True) user = UserBasicSerializer(read_only=True) class Meta: model = OrganizationMembership fields = [ "id", "user", "membership_id", # TODO: DEPRECATED in favor of `id` (for consistency) "user_id", # TODO: DEPRECATED in favor of `user` "user_first_name", # TODO: DEPRECATED in favor of `user` "user_email", # TODO: DEPRECATED in favor of `user` "level", "joined_at", "updated_at", ] read_only_fields = ["user_id", "joined_at", "updated_at"] def update(self, updated_membership, validated_data, **kwargs): updated_membership = cast(OrganizationMembership, updated_membership) raise_errors_on_nested_writes("update", self, validated_data) requesting_membership: OrganizationMembership = OrganizationMembership.objects.get( organization=updated_membership.organization, user=self.context["request"].user) for attr, value in validated_data.items(): if attr == "level": requesting_membership.validate_update(updated_membership, value) setattr(updated_membership, attr, value) updated_membership.save() return updated_membership
class InsightSerializer(serializers.ModelSerializer): result = serializers.SerializerMethodField() created_by = UserBasicSerializer(read_only=True) class Meta: model = DashboardItem fields = [ "id", "name", "filters", "filters_hash", "order", "deleted", "dashboard", "layouts", "color", "last_refresh", "refreshing", "result", "created_at", "saved", "created_by", ] read_only_fields = ( "created_by", "created_at", ) def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> DashboardItem: request = self.context["request"] team = Team.objects.get(id=self.context["team_id"]) validated_data.pop("last_refresh", None) # last_refresh sometimes gets sent if dashboard_item is duplicated if not validated_data.get("dashboard", None): dashboard_item = DashboardItem.objects.create(team=team, created_by=request.user, **validated_data) return dashboard_item elif validated_data["dashboard"].team == team: created_by = validated_data.pop("created_by", request.user) dashboard_item = DashboardItem.objects.create( team=team, last_refresh=now(), created_by=created_by, **validated_data ) return dashboard_item else: raise serializers.ValidationError("Dashboard not found") def get_result(self, dashboard_item: DashboardItem): if not dashboard_item.filters: return None result = get_safe_cache(dashboard_item.filters_hash) if not result or result.get("task_id", None): return None # Data might not be defined if there is still cached results from before moving from 'results' to 'data' return result.get("data") def to_representation(self, instance): representation = super().to_representation(instance) representation["filters"] = instance.dashboard_filters() return representation
class DashboardCollaboratorSerializer(serializers.ModelSerializer): user = UserBasicSerializer(read_only=True) dashboard_id = serializers.IntegerField(read_only=True) user_uuid = serializers.UUIDField(required=True, write_only=True) class Meta: model = DashboardPrivilege fields = [ "id", "dashboard_id", "user", "level", "added_at", "updated_at", "user_uuid", # write_only (see above) ] read_only_fields = ["id", "dashboard_id", "user", "user"] def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]: dashboard: Dashboard = self.context["dashboard"] if dashboard.effective_restriction_level <= Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT: raise exceptions.ValidationError( "Cannot add collaborators to a dashboard on the lowest restriction level." ) attrs = super().validate(attrs) level = attrs.get("level") if level is not None and level != Dashboard.PrivilegeLevel.CAN_EDIT: raise serializers.ValidationError( "Only edit access can be explicitly specified currently.") return attrs def create(self, validated_data): dashboard: Dashboard = self.context["dashboard"] user_uuid = validated_data.pop("user_uuid") try: validated_data["user"] = User.objects.filter(is_active=True).get( uuid=user_uuid) except User.DoesNotExist: raise serializers.ValidationError("User does not exist.") if cast(Team, dashboard.team).get_effective_membership_level( validated_data["user"].id) is None: raise exceptions.ValidationError( "Cannot add collaborators that have no access to the project.") if dashboard.can_user_restrict(validated_data["user"].id): raise exceptions.ValidationError( "Cannot add collaborators that already have inherent access (the dashboard owner or a project admins)." ) validated_data["dashboard_id"] = self.context["dashboard_id"] try: return super().create(validated_data) except IntegrityError: raise serializers.ValidationError( "User already is a collaborator.")
class OrganizationInviteSerializer(serializers.ModelSerializer): created_by = UserBasicSerializer(read_only=True) class Meta: model = OrganizationInvite fields = [ "id", "target_email", "first_name", "emailing_attempt_made", "is_expired", "created_by", "created_at", "updated_at", ] read_only_fields = [ "id", "emailing_attempt_made", "created_at", "updated_at", ] extra_kwargs = {"target_email": {"required": True, "allow_null": False}} def create(self, validated_data: Dict[str, Any], *args: Any, **kwargs: Any) -> OrganizationInvite: if OrganizationMembership.objects.filter( organization_id=self.context["organization_id"], user__email=validated_data["target_email"] ).exists(): raise exceptions.ValidationError("A user with this email address already belongs to the organization.") invite: OrganizationInvite = OrganizationInvite.objects.create( organization_id=self.context["organization_id"], created_by=self.context["request"].user, **validated_data, ) if is_email_available(with_absolute_urls=True): invite.emailing_attempt_made = True send_invite.delay(invite_id=invite.id) invite.save() report_team_member_invited( self.context["request"].user, invite_id=str(invite.id), name_provided=bool(validated_data.get("first_name")), current_invite_count=invite.organization.active_invites.count(), current_member_count=OrganizationMembership.objects.filter( organization_id=self.context["organization_id"], ).count(), is_bulk=self.context.get("bulk_create", False), email_available=is_email_available(with_absolute_urls=True), ) return invite
class AnnotationSerializer(serializers.ModelSerializer): created_by = UserBasicSerializer(read_only=True) class Meta: model = Annotation fields = [ "id", "content", "date_marker", "creation_type", "dashboard_item", "created_by", "created_at", "updated_at", "deleted", "scope", ] read_only_fields = [ "id", "creation_type", "created_by", "created_at", "updated_at", ] def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Annotation: request = self.context["request"] project = Team.objects.get(id=self.context["team_id"]) annotation = Annotation.objects.create( organization=project.organization, team=project, created_by=request.user, **validated_data, ) return annotation
class ActionSerializer(TaggedItemSerializerMixin, serializers.HyperlinkedModelSerializer): steps = ActionStepSerializer(many=True, required=False) created_by = UserBasicSerializer(read_only=True) is_calculating = serializers.SerializerMethodField() class Meta: model = Action fields = [ "id", "name", "description", "tags", "post_to_slack", "slack_message_format", "steps", "created_at", "created_by", "deleted", "is_calculating", "last_calculated_at", "team_id", ] extra_kwargs = {"team_id": {"read_only": True}} def get_is_calculating(self, action: Action) -> bool: return False def validate(self, attrs): instance = cast(Action, self.instance) exclude_args = {} if instance: include_args = {"team": instance.team} exclude_args = {"id": instance.pk} else: attrs["team_id"] = self.context["view"].team_id include_args = {"team_id": attrs["team_id"]} colliding_action_ids = list( Action.objects.filter(name=attrs["name"], deleted=False, **include_args) .exclude(**exclude_args)[:1] .values_list("id", flat=True) ) if colliding_action_ids: raise serializers.ValidationError( {"name": f"This project already has an action with this name, ID {colliding_action_ids[0]}"}, code="unique", ) return attrs def create(self, validated_data: Any) -> Any: steps = validated_data.pop("steps", []) validated_data["created_by"] = self.context["request"].user instance = super().create(validated_data) for step in steps: ActionStep.objects.create( action=instance, **{key: value for key, value in step.items() if key not in ("isNew", "selection")}, ) report_user_action(validated_data["created_by"], "action created", instance.get_analytics_metadata()) return instance def update(self, instance: Any, validated_data: Dict[str, Any]) -> Any: steps = validated_data.pop("steps", None) # If there's no steps property at all we just ignore it # If there is a step property but it's an empty array [], we'll delete all the steps if steps is not None: # remove steps not in the request step_ids = [step["id"] for step in steps if step.get("id")] instance.steps.exclude(pk__in=step_ids).delete() for step in steps: if step.get("id"): step_instance = ActionStep.objects.get(pk=step["id"]) step_serializer = ActionStepSerializer(instance=step_instance) step_serializer.update(step_instance, step) else: ActionStep.objects.create( action=instance, **{key: value for key, value in step.items() if key not in ("isNew", "selection")}, ) instance = super().update(instance, validated_data) instance.refresh_from_db() report_user_action( self.context["request"].user, "action updated", { **instance.get_analytics_metadata(), "updated_by_creator": self.context["request"].user == instance.created_by, }, ) return instance
def to_representation(self, instance): serializer = UserBasicSerializer(instance=instance) return serializer.data
def to_representation(self, instance) -> Dict: data = UserBasicSerializer(instance=instance).data data["redirect_url"] = "/ingestion" if not settings.DEMO else "/" return data
class FeatureFlagSerializer(serializers.HyperlinkedModelSerializer): created_by = UserBasicSerializer(read_only=True) # :TRICKY: Needed for backwards compatibility filters = serializers.DictField(source="get_filters", required=False) is_simple_flag = serializers.SerializerMethodField() rollout_percentage = serializers.SerializerMethodField() class Meta: model = FeatureFlag fields = [ "id", "name", "key", "filters", "deleted", "active", "created_by", "created_at", "is_simple_flag", "rollout_percentage", ] # Simple flags are ones that only have rollout_percentage # That means server side libraries are able to gate these flags without calling to the server def get_is_simple_flag(self, feature_flag: FeatureFlag) -> bool: return len(feature_flag.groups) == 1 and all( len(group.get("properties", [])) == 0 for group in feature_flag.groups) def get_rollout_percentage(self, feature_flag: FeatureFlag) -> Optional[int]: if self.get_is_simple_flag(feature_flag): return feature_flag.groups[0].get("rollout_percentage") else: return None def validate_key(self, value): exclude_kwargs = {} if self.instance: exclude_kwargs = {"pk": cast(FeatureFlag, self.instance).pk} if (FeatureFlag.objects.filter( key=value, team_id=self.context["team_id"], deleted=False).exclude(**exclude_kwargs).exists()): raise serializers.ValidationError( "There is already a feature flag with this key.", code="unique") return value def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> FeatureFlag: request = self.context["request"] validated_data["created_by"] = request.user validated_data["team"] = request.user.team try: feature_flag = super().create(validated_data) except IntegrityError: if self.perform_destroy(validated_data["key"]): feature_flag = super().create(validated_data) else: raise serializers.ValidationError("This key already exists.", code="key-exists") posthoganalytics.capture( request.user.distinct_id, "feature flag created", instance.get_analytics_metadata(), ) return instance def perform_destroy(self, key: str) -> bool: featureFlag = FeatureFlag.objects.all().filter(deleted=True, key=key).first() if featureFlag: featureFlag.delete() return True return False
def to_representation(self, instance): representation = super().to_representation(instance) representation["owner"] = UserBasicSerializer( instance=instance.owner).data return representation
class DashboardSerializer(TaggedItemSerializerMixin, serializers.ModelSerializer): items = serializers.SerializerMethodField() created_by = UserBasicSerializer(read_only=True) use_template = serializers.CharField(write_only=True, allow_blank=True, required=False) use_dashboard = serializers.IntegerField(write_only=True, allow_null=True, required=False) effective_privilege_level = serializers.SerializerMethodField() class Meta: model = Dashboard fields = [ "id", "name", "description", "pinned", "items", "created_at", "created_by", "is_shared", "share_token", "deleted", "creation_mode", "use_template", "use_dashboard", "filters", "tags", "restriction_level", "effective_restriction_level", "effective_privilege_level", ] read_only_fields = [ "creation_mode", "effective_restriction_level", ] def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Dashboard: request = self.context["request"] validated_data["created_by"] = request.user team = Team.objects.get(id=self.context["team_id"]) use_template: str = validated_data.pop("use_template", None) use_dashboard: int = validated_data.pop("use_dashboard", None) validated_data = self._update_creation_mode(validated_data, use_template, use_dashboard) tags = validated_data.pop( "tags", None ) # tags are created separately below as global tag relationships dashboard = Dashboard.objects.create(team=team, **validated_data) if use_template: try: create_dashboard_from_template(use_template, dashboard) except AttributeError: raise serializers.ValidationError( {"use_template": "Invalid value provided."}) elif use_dashboard: try: from posthog.api.insight import InsightSerializer existing_dashboard = Dashboard.objects.get(id=use_dashboard, team=team) existing_dashboard_items = existing_dashboard.items.all() for dashboard_item in existing_dashboard_items: override_dashboard_item_data = { "id": None, # to create a new Insight "dashboard": dashboard.pk, "last_refresh": now(), } new_data = { **InsightSerializer( dashboard_item, context=self.context, ).data, **override_dashboard_item_data, } new_tags = new_data.pop("tags", None) insight_serializer = InsightSerializer( data=new_data, context=self.context, ) insight_serializer.is_valid() insight_serializer.save() # Create new insight's tags separately. Force create tags on dashboard duplication. self._attempt_set_tags(new_tags, insight_serializer.instance, force_create=True) except Dashboard.DoesNotExist: raise serializers.ValidationError( {"use_dashboard": "Invalid value provided"}) elif request.data.get("items"): for item in request.data["items"]: Insight.objects.create( **{ key: value for key, value in item.items() if key not in ("id", "deleted", "dashboard", "team") }, dashboard=dashboard, team=team, ) # Manual tag creation since this create method doesn't call super() self._attempt_set_tags(tags, dashboard) report_user_action( request.user, "dashboard created", { **dashboard.get_analytics_metadata(), "from_template": bool(use_template), "template_key": use_template, "duplicated": bool(use_dashboard), "dashboard_id": use_dashboard, }, ) return dashboard def update( self, instance: Dashboard, validated_data: Dict, *args: Any, **kwargs: Any, ) -> Dashboard: user = cast(User, self.context["request"].user) can_user_restrict = instance.can_user_restrict(user.id) if "restriction_level" in validated_data and not can_user_restrict: raise exceptions.PermissionDenied( "Only the dashboard owner and project admins have the restriction rights required to change the dashboard's restriction level." ) validated_data.pop("use_template", None) # Remove attribute if present if validated_data.get("is_shared") and not instance.share_token: instance.share_token = secrets.token_urlsafe(22) instance = super().update(instance, validated_data) if "request" in self.context: report_user_action(user, "dashboard updated", instance.get_analytics_metadata()) return instance def add_dive_source_item(self, items: QuerySet, dive_source_id: int): item_as_list = list(i for i in items if i.id == dive_source_id) if not item_as_list: item_as_list = [Insight.objects.get(pk=dive_source_id)] others = list(i for i in items if i.id != dive_source_id) return item_as_list + others def get_items(self, dashboard: Dashboard): if self.context["view"].action == "list": return None items = dashboard.items.filter(deleted=False).order_by("order").all() self.context.update({"dashboard": dashboard}) dive_source_id = self.context["request"].GET.get("dive_source_id") if dive_source_id is not None: items = self.add_dive_source_item(items, int(dive_source_id)) # Make sure all items have an insight set # This should have only happened historically for item in items: if not item.filters.get("insight"): item.filters["insight"] = INSIGHT_TRENDS item.save() return InsightSerializer(items, many=True, context=self.context).data def get_effective_privilege_level( self, dashboard: Dashboard) -> Dashboard.PrivilegeLevel: return dashboard.get_effective_privilege_level( self.context["request"].user.id) def validate(self, data): if data.get("use_dashboard", None) and data.get("use_template", None): raise serializers.ValidationError( "`use_dashboard` and `use_template` cannot be used together") return data def _update_creation_mode(self, validated_data, use_template: str, use_dashboard: int): if use_template: return {**validated_data, "creation_mode": "template"} if use_dashboard: return {**validated_data, "creation_mode": "duplicate"} return {**validated_data, "creation_mode": "default"}
class SubscriptionSerializer(serializers.ModelSerializer): """Standard Subscription serializer.""" created_by = UserBasicSerializer(read_only=True) invite_message = serializers.CharField(required=False, allow_blank=True, allow_null=True) class Meta: model = Subscription fields = [ "id", "dashboard", "insight", "target_type", "target_value", "frequency", "interval", "byweekday", "bysetpos", "count", "start_date", "until_date", "created_at", "created_by", "deleted", "title", "summary", "next_delivery_date", "invite_message", ] read_only_fields = [ "id", "created_at", "created_by", "next_delivery_date", "summary", ] def validate(self, attrs): if not self.initial_data: # Create if not attrs.get("dashboard") and not attrs.get("insight"): raise ValidationError("Either dashboard or insight is required for an export.") if attrs.get("dashboard") and attrs["dashboard"].team.id != self.context["team_id"]: raise ValidationError({"dashboard": ["This dashboard does not belong to your team."]}) if attrs.get("insight") and attrs["insight"].team.id != self.context["team_id"]: raise ValidationError({"insight": ["This insight does not belong to your team."]}) return attrs def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Subscription: request = self.context["request"] validated_data["team_id"] = self.context["team_id"] validated_data["created_by"] = request.user invite_message = validated_data.pop("invite_message", "") instance: Subscription = super().create(validated_data) subscriptions.handle_subscription_value_change.delay(instance.id, "", invite_message) return instance def update(self, instance: Subscription, validated_data: dict, *args: Any, **kwargs: Any) -> Subscription: previous_value = instance.target_value invite_message = validated_data.pop("invite_message", "") instance = super().update(instance, validated_data) subscriptions.handle_subscription_value_change.delay(instance.id, previous_value, invite_message) return instance
class ActionSerializer(serializers.HyperlinkedModelSerializer): steps = ActionStepSerializer(many=True, required=False) count = serializers.SerializerMethodField() created_by = UserBasicSerializer(read_only=True) class Meta: model = Action fields = [ "id", "name", "post_to_slack", "slack_message_format", "steps", "created_at", "deleted", "count", "is_calculating", "last_calculated_at", "created_by", "team_id", ] extra_kwargs = {"team_id": {"read_only": True}} def get_count(self, action: Action) -> Optional[int]: if hasattr(action, "count"): return action.count # type: ignore return None def _calculate_action(self, action: Action) -> None: calculate_action.delay(action_id=action.pk) def validate(self, attrs): exclude_args = {} if self.instance: include_args = {"team": self.instance.team} exclude_args = {"id": self.instance.pk} else: attrs["team_id"] = self.context["view"].team_id include_args = {"team_id": attrs["team_id"]} if Action.objects.filter( name=attrs["name"], deleted=False, **include_args).exclude(**exclude_args).exists(): raise serializers.ValidationError( {"name": "This project already has an action with that name."}, code="unique") return attrs def create(self, validated_data: Any) -> Any: steps = validated_data.pop("steps", []) validated_data["created_by"] = self.context["request"].user instance = super().create(validated_data) for step in steps: ActionStep.objects.create( action=instance, **{ key: value for key, value in step.items() if key not in ("isNew", "selection") }, ) self._calculate_action(instance) posthoganalytics.capture(validated_data["created_by"].distinct_id, "action created", instance.get_analytics_metadata()) return instance def update(self, instance: Any, validated_data: Dict[str, Any]) -> Any: steps = validated_data.pop("steps", None) # If there's no steps property at all we just ignore it # If there is a step property but it's an empty array [], we'll delete all the steps if steps is not None: # remove steps not in the request step_ids = [step["id"] for step in steps if step.get("id")] instance.steps.exclude(pk__in=step_ids).delete() for step in steps: if step.get("id"): step_instance = ActionStep.objects.get(pk=step["id"]) step_serializer = ActionStepSerializer( instance=step_instance) step_serializer.update(step_instance, step) else: ActionStep.objects.create( action=instance, **{ key: value for key, value in step.items() if key not in ("isNew", "selection") }, ) instance = super().update(instance, validated_data) self._calculate_action(instance) instance.refresh_from_db() posthoganalytics.capture( self.context["request"].user.distinct_id, "action updated", { **instance.get_analytics_metadata(), "updated_by_creator": self.context["request"].user == instance.created_by, }, ) return instance
class CohortSerializer(serializers.ModelSerializer): created_by = UserBasicSerializer(read_only=True) earliest_timestamp_func = get_earliest_timestamp class Meta: model = Cohort fields = [ "id", "name", "description", "groups", "deleted", "is_calculating", "created_by", "created_at", "last_calculation", "errors_calculating", "count", "is_static", ] read_only_fields = [ "id", "is_calculating", "created_by", "created_at", "last_calculation", "errors_calculating", "count", ] def _handle_static(self, cohort: Cohort, request: Request): if request.FILES.get("csv"): self._calculate_static_by_csv(request.FILES["csv"], cohort) else: filter_data = request.GET.dict() if filter_data: insert_cohort_from_insight_filter.delay(cohort.pk, filter_data) def _handle_csv(self, file, cohort: Cohort) -> None: decoded_file = file.read().decode("utf-8").splitlines() reader = csv.reader(decoded_file) distinct_ids_and_emails = [ row[0] for row in reader if len(row) > 0 and row ] calculate_cohort_from_list.delay(cohort.pk, distinct_ids_and_emails) def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Cohort: request = self.context["request"] validated_data["created_by"] = request.user if not validated_data.get("is_static"): validated_data["is_calculating"] = True cohort = Cohort.objects.create(team_id=self.context["team_id"], **validated_data) if cohort.is_static: self._handle_static(cohort, request) else: pending_version = get_and_update_pending_version(cohort) calculate_cohort_ch.delay(cohort.id, pending_version) report_user_action(request.user, "cohort created", cohort.get_analytics_metadata()) return cohort def _calculate_static_by_csv(self, file, cohort: Cohort) -> None: decoded_file = file.read().decode("utf-8").splitlines() reader = csv.reader(decoded_file) distinct_ids_and_emails = [ row[0] for row in reader if len(row) > 0 and row ] calculate_cohort_from_list.delay(cohort.pk, distinct_ids_and_emails) def update(self, cohort: Cohort, validated_data: Dict, *args: Any, **kwargs: Any) -> Cohort: # type: ignore request = self.context["request"] cohort.name = validated_data.get("name", cohort.name) cohort.description = validated_data.get("description", cohort.description) cohort.groups = validated_data.get("groups", cohort.groups) cohort.is_static = validated_data.get("is_static", cohort.is_static) deleted_state = validated_data.get("deleted", None) is_deletion_change = deleted_state is not None and cohort.deleted != deleted_state if is_deletion_change: cohort.deleted = deleted_state if not cohort.is_static and not is_deletion_change: cohort.is_calculating = True cohort.save() if not deleted_state: if cohort.is_static: # You can't update a static cohort using the trend/stickiness thing if request.FILES.get("csv"): self._calculate_static_by_csv(request.FILES["csv"], cohort) else: # Increment based on pending versions pending_version = get_and_update_pending_version(cohort) calculate_cohort_ch.delay(cohort.id, pending_version) report_user_action( request.user, "cohort updated", { **cohort.get_analytics_metadata(), "updated_by_creator": request.user == cohort.created_by }, ) return cohort
class EnterpriseEventDefinitionSerializer(TaggedItemSerializerMixin, serializers.ModelSerializer): updated_by = UserBasicSerializer(read_only=True) verified_by = UserBasicSerializer(read_only=True) created_by = UserBasicSerializer(read_only=True) is_action = serializers.SerializerMethodField(read_only=True) action_id = serializers.IntegerField(read_only=True) is_calculating = serializers.BooleanField(read_only=True) last_calculated_at = serializers.DateTimeField(read_only=True) last_updated_at = serializers.DateTimeField(read_only=True) post_to_slack = serializers.BooleanField(default=False) class Meta: model = EnterpriseEventDefinition fields = ( "id", "name", "owner", "description", "tags", "volume_30_day", "query_usage_30_day", "created_at", "updated_at", "updated_by", "last_seen_at", "last_updated_at", "verified", "verified_at", "verified_by", # Action fields "is_action", "action_id", "is_calculating", "last_calculated_at", "created_by", "post_to_slack", ) read_only_fields = [ "id", "name", "created_at", "updated_at", "volume_30_day", "query_usage_30_day", "last_seen_at", "last_updated_at", "verified_at", "verified_by", # Action fields "is_action", "action_id", "is_calculating", "last_calculated_at", "created_by", ] def update(self, event_definition: EnterpriseEventDefinition, validated_data): validated_data["updated_by"] = self.context["request"].user if "verified" in validated_data: if validated_data["verified"] and not event_definition.verified: # Verify event 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 event 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 event, invalid action. Ignore attribute. validated_data.pop("verified") return super().update(event_definition, validated_data) def to_representation(self, instance): representation = super().to_representation(instance) representation["owner"] = (UserBasicSerializer( instance=instance.owner).data if hasattr(instance, "owner") and instance.owner else None) return representation def get_is_action(self, obj): return hasattr(obj, "action_id") and obj.action_id is not None
class InsightSerializer(TaggedItemSerializerMixin, InsightBasicSerializer): result = serializers.SerializerMethodField() last_refresh = serializers.SerializerMethodField() created_by = UserBasicSerializer(read_only=True) last_modified_by = UserBasicSerializer(read_only=True) effective_privilege_level = serializers.SerializerMethodField() class Meta: model = Insight fields = [ "id", "short_id", "name", "derived_name", "filters", "filters_hash", "order", "deleted", "dashboard", "layouts", "color", "last_refresh", "refreshing", "result", "created_at", "created_by", "description", "updated_at", "tags", "favorited", "saved", "last_modified_at", "last_modified_by", "is_sample", "effective_restriction_level", "effective_privilege_level", ] read_only_fields = ( "created_at", "created_by", "last_modified_at", "last_modified_by", "short_id", "updated_at", "is_sample", "effective_restriction_level", "effective_privilege_level", ) def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Insight: request = self.context["request"] team = Team.objects.get(id=self.context["team_id"]) validated_data.pop( "last_refresh", None ) # last_refresh sometimes gets sent if dashboard_item is duplicated tags = validated_data.pop( "tags", None) # tags are created separately as global tag relationships if not validated_data.get("dashboard", None): dashboard_item = Insight.objects.create( team=team, created_by=request.user, last_modified_by=request.user, **validated_data) elif validated_data["dashboard"].team == team: created_by = validated_data.pop("created_by", request.user) dashboard_item = Insight.objects.create( team=team, last_refresh=now(), created_by=created_by, last_modified_by=created_by, **validated_data) else: raise serializers.ValidationError("Dashboard not found") # Manual tag creation since this create method doesn't call super() self._attempt_set_tags(tags, dashboard_item) return dashboard_item def update(self, instance: Insight, validated_data: Dict, **kwargs) -> Insight: # Remove is_sample if it's set as user has altered the sample configuration validated_data["is_sample"] = False if validated_data.keys() & Insight.MATERIAL_INSIGHT_FIELDS: instance.last_modified_at = now() instance.last_modified_by = self.context["request"].user return super().update(instance, validated_data) def get_result(self, insight: Insight): if not insight.filters: return None if should_refresh(self.context["request"]): return update_dashboard_item_cache(insight, None) result = get_safe_cache(insight.filters_hash) if not result or result.get("task_id", None): return None # Data might not be defined if there is still cached results from before moving from 'results' to 'data' return result.get("result") def get_last_refresh(self, insight: Insight): if should_refresh(self.context["request"]): return now() result = self.get_result(insight) if result is not None: return insight.last_refresh if insight.last_refresh is not None: # Update last_refresh without updating "updated_at" (insight edit date) insight.last_refresh = None insight.save() return None def get_effective_privilege_level( self, insight: Insight) -> Dashboard.PrivilegeLevel: return insight.get_effective_privilege_level( self.context["request"].user.id) def to_representation(self, instance: Insight): representation = super().to_representation(instance) representation["filters"] = instance.dashboard_filters( dashboard=self.context.get("dashboard")) return representation
class DashboardSerializer(serializers.ModelSerializer): items = serializers.SerializerMethodField() created_by = UserBasicSerializer(read_only=True) use_template = serializers.CharField(write_only=True, allow_blank=True, required=False) use_dashboard = serializers.IntegerField(write_only=True, allow_null=True, required=False) class Meta: model = Dashboard fields = [ "id", "name", "description", "pinned", "items", "created_at", "created_by", "is_shared", "share_token", "deleted", "creation_mode", "use_template", "use_dashboard", "filters", "tags", ] read_only_fields = ("creation_mode", ) def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Dashboard: request = self.context["request"] validated_data["created_by"] = request.user team = Team.objects.get(id=self.context["team_id"]) use_template: str = validated_data.pop("use_template", None) use_dashboard: int = validated_data.pop("use_dashboard", None) validated_data = self._update_creation_mode(validated_data, use_template, use_dashboard) dashboard = Dashboard.objects.create(team=team, **validated_data) if use_template: try: create_dashboard_from_template(use_template, dashboard) except AttributeError: raise serializers.ValidationError( {"use_template": "Invalid value provided."}) elif use_dashboard: try: from posthog.api.insight import InsightSerializer existing_dashboard = Dashboard.objects.get(id=use_dashboard, team=team) existing_dashboard_items = existing_dashboard.items.all() for dashboard_item in existing_dashboard_items: override_dashboard_item_data = { "id": None, # to create a new Insight "dashboard": dashboard.pk, "last_refresh": now(), } insight_serializer = InsightSerializer( data={ **InsightSerializer( dashboard_item, context=self.context, ).data, **override_dashboard_item_data, }, context=self.context, ) insight_serializer.is_valid() insight_serializer.save() except Dashboard.DoesNotExist: raise serializers.ValidationError( {"use_dashboard": "Invalid value provided"}) elif request.data.get("items"): for item in request.data["items"]: Insight.objects.create( **{ key: value for key, value in item.items() if key not in ("id", "deleted", "dashboard", "team") }, dashboard=dashboard, team=team, ) posthoganalytics.capture( request.user.distinct_id, "dashboard created", { **dashboard.get_analytics_metadata(), "from_template": bool(use_template), "template_key": use_template, "duplicated": bool(use_dashboard), "dashboard_id": use_dashboard, }, ) return dashboard def update( self, instance: Dashboard, validated_data: Dict, *args: Any, **kwargs: Any, ) -> Dashboard: validated_data.pop("use_template", None) # Remove attribute if present if validated_data.get("is_shared") and not instance.share_token: instance.share_token = secrets.token_urlsafe(22) instance = super().update(instance, validated_data) if "request" in self.context: posthoganalytics.capture(self.context["request"].user.distinct_id, "dashboard updated", instance.get_analytics_metadata()) return instance def add_dive_source_item(self, items: QuerySet, dive_source_id: int): item_as_list = list(i for i in items if i.id == dive_source_id) if not item_as_list: item_as_list = [Insight.objects.get(pk=dive_source_id)] others = list(i for i in items if i.id != dive_source_id) return item_as_list + others def get_items(self, dashboard: Dashboard): if self.context["view"].action == "list": return None if self.context["request"].GET.get("refresh"): update_dashboard_items_cache(dashboard) dashboard.refresh_from_db() items = dashboard.items.filter(deleted=False).order_by("order").all() self.context.update({"dashboard": dashboard}) dive_source_id = self.context["request"].GET.get("dive_source_id") if dive_source_id is not None: items = self.add_dive_source_item(items, int(dive_source_id)) return DashboardItemSerializer(items, many=True, context=self.context).data def validate(self, data): if data.get("use_dashboard", None) and data.get("use_template", None): raise serializers.ValidationError( "`use_dashboard` and `use_template` cannot be used together") return data def _update_creation_mode(self, validated_data, use_template: str, use_dashboard: int): if use_template: return {**validated_data, "creation_mode": "template"} if use_dashboard: return {**validated_data, "creation_mode": "duplicate"} return {**validated_data, "creation_mode": "default"}
class DashboardSerializer(serializers.ModelSerializer): items = serializers.SerializerMethodField() created_by = UserBasicSerializer(read_only=True) use_template = serializers.CharField(write_only=True, allow_blank=True, required=False) class Meta: model = Dashboard fields = [ "id", "name", "description", "pinned", "items", "created_at", "created_by", "is_shared", "share_token", "deleted", "creation_mode", "use_template", "filters", "tags", ] read_only_fields = ("creation_mode", ) def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Dashboard: request = self.context["request"] validated_data["created_by"] = request.user team = Team.objects.get(id=self.context["team_id"]) use_template: str = validated_data.pop("use_template", None) creation_mode = "template" if use_template else "default" dashboard = Dashboard.objects.create(team=team, creation_mode=creation_mode, **validated_data) if use_template: try: create_dashboard_from_template(use_template, dashboard) except AttributeError: raise serializers.ValidationError( {"use_template": "Invalid value provided."}) elif request.data.get("items"): for item in request.data["items"]: DashboardItem.objects.create( **{ key: value for key, value in item.items() if key not in ("id", "deleted", "dashboard", "team") }, dashboard=dashboard, team=team, ) posthoganalytics.capture( request.user.distinct_id, "dashboard created", { **dashboard.get_analytics_metadata(), "from_template": bool(use_template), "template_key": use_template }, ) return dashboard def update( self, instance: Dashboard, validated_data: Dict, *args: Any, **kwargs: Any, ) -> Dashboard: validated_data.pop("use_template", None) # Remove attribute if present if validated_data.get("is_shared") and not instance.share_token: instance.share_token = secrets.token_urlsafe(22) instance = super().update(instance, validated_data) if "request" in self.context: posthoganalytics.capture(self.context["request"].user.distinct_id, "dashboard updated", instance.get_analytics_metadata()) return instance def get_items(self, dashboard: Dashboard): if self.context["view"].action == "list": return None if self.context["request"].GET.get("refresh"): update_dashboard_items_cache(dashboard) items = dashboard.items.filter(deleted=False).order_by("order").all() self.context.update({"dashboard": dashboard}) return DashboardItemSerializer(items, many=True, context=self.context).data
def to_representation(self, instance) -> Dict: data = UserBasicSerializer(instance=instance).data data[ "redirect_url"] = "/personalization" if self.enable_new_onboarding( ) else "/ingestion" return data
class FeatureFlagSerializer(serializers.HyperlinkedModelSerializer): created_by = UserBasicSerializer(read_only=True) # :TRICKY: Needed for backwards compatibility filters = serializers.DictField(source="get_filters", required=False) is_simple_flag = serializers.SerializerMethodField() rollout_percentage = serializers.SerializerMethodField() class Meta: model = FeatureFlag fields = [ "id", "name", "key", "filters", "deleted", "active", "created_by", "created_at", "is_simple_flag", "rollout_percentage", ] # Simple flags are ones that only have rollout_percentage # That means server side libraries are able to gate these flags without calling to the server def get_is_simple_flag(self, feature_flag: FeatureFlag) -> bool: no_properties_used = all( len(condition.get("properties", [])) == 0 for condition in feature_flag.conditions) return (len(feature_flag.conditions) == 1 and no_properties_used and feature_flag.aggregation_group_type_index is None) def get_rollout_percentage(self, feature_flag: FeatureFlag) -> Optional[int]: if self.get_is_simple_flag(feature_flag): return feature_flag.conditions[0].get("rollout_percentage") else: return None def validate_key(self, value): exclude_kwargs = {} if self.instance: exclude_kwargs = {"pk": cast(FeatureFlag, self.instance).pk} if (FeatureFlag.objects.filter( key=value, team_id=self.context["team_id"], deleted=False).exclude(**exclude_kwargs).exists()): raise serializers.ValidationError( "There is already a feature flag with this key.", code="unique") return value def validate_filters(self, filters): aggregation_group_type_index = filters.get( "aggregation_group_type_index", None) def properties_all_match(predicate): return all( predicate(Property(**property)) for condition in filters["groups"] for property in condition.get("properties", [])) if aggregation_group_type_index is None: is_valid = properties_all_match( lambda prop: prop.type in ["person", "cohort"]) if not is_valid: raise serializers.ValidationError( "Filters are not valid (can only use person and cohort properties)" ) else: is_valid = properties_all_match( lambda prop: prop.type == "group" and prop.group_type_index == aggregation_group_type_index) if not is_valid: raise serializers.ValidationError( "Filters are not valid (can only use group properties)") return filters def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> FeatureFlag: request = self.context["request"] validated_data["created_by"] = request.user validated_data["team_id"] = self.context["team_id"] self._update_filters(validated_data) variants = (validated_data.get("filters", {}).get("multivariate", {}) or {}).get("variants", []) variant_rollout_sum = 0 for variant in variants: variant_rollout_sum += variant.get("rollout_percentage") if len(variants) > 0 and variant_rollout_sum != 100: raise exceptions.ValidationError( "Invalid variant definitions: Variant rollout percentages must sum to 100." ) FeatureFlag.objects.filter(key=validated_data["key"], team=self.context["team_id"], deleted=True).delete() instance: FeatureFlag = super().create(validated_data) instance.update_cohorts() report_user_action( request.user, "feature flag created", instance.get_analytics_metadata(), ) return instance def update(self, instance: FeatureFlag, validated_data: Dict, *args: Any, **kwargs: Any) -> FeatureFlag: request = self.context["request"] validated_key = validated_data.get("key", None) if validated_key: FeatureFlag.objects.filter(key=validated_key, team=instance.team, deleted=True).delete() self._update_filters(validated_data) instance = super().update(instance, validated_data) instance.update_cohorts() report_user_action( request.user, "feature flag updated", instance.get_analytics_metadata(), ) return instance def _update_filters(self, validated_data): if "get_filters" in validated_data: validated_data["filters"] = validated_data.pop("get_filters")
class CohortSerializer(serializers.ModelSerializer): created_by = UserBasicSerializer(read_only=True) count = serializers.SerializerMethodField() earliest_timestamp_func = lambda team_id: Event.objects.earliest_timestamp( team_id) class Meta: model = Cohort fields = [ "id", "name", "groups", "deleted", "is_calculating", "created_by", "created_at", "last_calculation", "errors_calculating", "count", "is_static", ] read_only_fields = [ "id", "is_calculating", "created_by", "created_at", "last_calculation", "errors_calculating", "count", ] def _handle_csv(self, file, cohort: Cohort) -> None: decoded_file = file.read().decode("utf-8").splitlines() reader = csv.reader(decoded_file) distinct_ids_and_emails = [ row[0] for row in reader if len(row) > 0 and row ] calculate_cohort_from_list.delay(cohort.pk, distinct_ids_and_emails) def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Cohort: request = self.context["request"] validated_data["created_by"] = request.user if not validated_data.get("is_static"): validated_data["is_calculating"] = True cohort = Cohort.objects.create(team_id=self.context["team_id"], **validated_data) if cohort.is_static: self._handle_static(cohort, request) else: calculate_cohort.delay(cohort_id=cohort.pk) calculate_cohort_ch.delay(cohort_id=cohort.pk) posthoganalytics.capture(request.user.distinct_id, "cohort created", cohort.get_analytics_metadata()) return cohort def _handle_static(self, cohort: Cohort, request: Request): if request.FILES.get("csv"): self._calculate_static_by_csv(request.FILES["csv"], cohort) else: try: filter = Filter(request=request) team = cast(User, request.user).team target_entity = get_target_entity(request) if filter.shown_as == TRENDS_STICKINESS: stickiness_filter = StickinessFilter( request=request, team=team, get_earliest_timestamp=self.earliest_timestamp_func) self._handle_stickiness_people(target_entity, cohort, stickiness_filter) else: self._handle_trend_people(target_entity, cohort, filter) except Exception as e: capture_exception(e) raise ValueError("This cohort has no conditions") def _calculate_static_by_csv(self, file, cohort: Cohort) -> None: decoded_file = file.read().decode("utf-8").splitlines() reader = csv.reader(decoded_file) distinct_ids_and_emails = [ row[0] for row in reader if len(row) > 0 and row ] calculate_cohort_from_list.delay(cohort.pk, distinct_ids_and_emails) def _calculate_static_by_people(self, people: List[str], cohort: Cohort) -> None: calculate_cohort_from_list.delay(cohort.pk, people) def _handle_stickiness_people(self, target_entity: Entity, cohort: Cohort, filter: StickinessFilter) -> None: events = stickiness_process_entity_type(target_entity, cohort.team, filter) events = stickiness_format_intervals(events, filter) people = stickiness_fetch_people(events, cohort.team, filter) ids = [ person.distinct_ids[0] for person in people if len(person.distinct_ids) ] self._calculate_static_by_people(ids, cohort) def _handle_trend_people(self, target_entity: Entity, cohort: Cohort, filter: Filter) -> None: events = filter_by_type(entity=target_entity, team=cohort.team, filter=filter) people = calculate_people(team=cohort.team, events=events, filter=filter) ids = [ person.distinct_ids[0] for person in people if len(person.distinct_ids) ] self._calculate_static_by_people(ids, cohort) def update(self, cohort: Cohort, validated_data: Dict, *args: Any, **kwargs: Any) -> Cohort: # type: ignore request = self.context["request"] cohort.name = validated_data.get("name", cohort.name) cohort.groups = validated_data.get("groups", cohort.groups) deleted_state = validated_data.get("deleted", None) is_deletion_change = deleted_state is not None if is_deletion_change: cohort.deleted = deleted_state if not cohort.is_static and not is_deletion_change: cohort.is_calculating = True cohort.save() if not deleted_state: if cohort.is_static: self._handle_static(cohort, request) else: calculate_cohort.delay(cohort_id=cohort.pk) calculate_cohort_ch.delay(cohort_id=cohort.pk) posthoganalytics.capture( request.user.distinct_id, "cohort updated", { **cohort.get_analytics_metadata(), "updated_by_creator": request.user == cohort.created_by }, ) return cohort def get_count(self, action: Cohort) -> Optional[int]: if hasattr(action, "count"): return action.count # type: ignore return None
def to_representation(self, instance): representation = super().to_representation(instance) representation["owner"] = (UserBasicSerializer( instance=instance.owner).data if hasattr(instance, "owner") and instance.owner else None) return representation
class ExplicitTeamMemberSerializer(serializers.ModelSerializer): user = UserBasicSerializer(source="parent_membership.user", read_only=True) parent_level = serializers.IntegerField(source="parent_membership.level", read_only=True) user_uuid = serializers.UUIDField(required=True, write_only=True) class Meta: model = ExplicitTeamMembership fields = [ "id", "level", "parent_level", "parent_membership_id", "joined_at", "updated_at", "user", "user_uuid", # write_only (see above) "effective_level", # read_only (calculated) ] read_only_fields = [ "id", "parent_membership_id", "joined_at", "updated_at", "user", "effective_level" ] def create(self, validated_data): team: Team = self.context["team"] user_uuid = validated_data.pop("user_uuid") validated_data["team"] = team try: requesting_parent_membership: OrganizationMembership = OrganizationMembership.objects.get( organization_id=team.organization_id, user__uuid=user_uuid, user__is_active=True, ) except OrganizationMembership.DoesNotExist: raise exceptions.PermissionDenied( "You both need to belong to the same organization.") validated_data["parent_membership"] = requesting_parent_membership try: return super().create(validated_data) except IntegrityError: raise exceptions.ValidationError( "This user likely already is an explicit member of the project." ) def validate(self, attrs): team: Team = self.context["team"] if not team.access_control: raise exceptions.ValidationError( "Explicit members can only be accessed for projects with project-based permissioning enabled." ) requesting_user: User = self.context["request"].user membership_being_accessed = cast(Optional[ExplicitTeamMembership], self.instance) try: requesting_level = self.context[ "team"].get_effective_membership_level(requesting_user.id) except OrganizationMembership.DoesNotExist: # Requesting user does not belong to the project's organization, so we spoof a 404 for enhanced security raise exceptions.NotFound("Project not found.") new_level = attrs.get("level") if requesting_level is None: raise exceptions.PermissionDenied( "You do not have the required access to this project.") if attrs.get("user_uuid") == requesting_user.uuid: # Create-only check raise exceptions.PermissionDenied( "You can't explicitly add yourself to projects.") if new_level is not None and new_level > requesting_level: raise exceptions.PermissionDenied( "You can only set access level to lower or equal to your current one." ) if membership_being_accessed is not None: # Update-only checks if membership_being_accessed.parent_membership.user_id != requesting_user.id: # Requesting user updating someone else if membership_being_accessed.level > requesting_level: raise exceptions.PermissionDenied( "You can only edit others with level lower or equal to you." ) else: # Requesting user updating themselves if new_level is not None: raise exceptions.PermissionDenied( "You can't set your own access level.") return attrs
class InsightSerializer(InsightBasicSerializer): result = serializers.SerializerMethodField() last_refresh = serializers.SerializerMethodField() created_by = UserBasicSerializer(read_only=True) class Meta: model = Insight fields = [ "id", "short_id", "name", "filters", "filters_hash", "order", "deleted", "dashboard", "dive_dashboard", "layouts", "color", "last_refresh", "refreshing", "result", "created_at", "description", "updated_at", "tags", "favorited", "saved", "created_by", "is_sample", ] read_only_fields = ("created_by", "created_at", "short_id", "updated_at", "is_sample") def create(self, validated_data: Dict, *args: Any, **kwargs: Any) -> Insight: request = self.context["request"] team = Team.objects.get(id=self.context["team_id"]) validated_data.pop( "last_refresh", None ) # last_refresh sometimes gets sent if dashboard_item is duplicated if not validated_data.get("dashboard", None) and not validated_data.get( "dive_dashboard", None): dashboard_item = Insight.objects.create(team=team, created_by=request.user, **validated_data) return dashboard_item elif validated_data["dashboard"].team == team: created_by = validated_data.pop("created_by", request.user) dashboard_item = Insight.objects.create(team=team, last_refresh=now(), created_by=created_by, **validated_data) return dashboard_item else: raise serializers.ValidationError("Dashboard not found") def update(self, instance: Insight, validated_data: Dict, **kwargs) -> Insight: # Remove is_sample if it's set as user has altered the sample configuration validated_data["is_sample"] = False return super().update(instance, validated_data) def get_result(self, insight: Insight): if not insight.filters: return None if self.context["request"].GET.get("refresh"): return update_dashboard_item_cache(insight, None) result = get_safe_cache(insight.filters_hash) if not result or result.get("task_id", None): return None # Data might not be defined if there is still cached results from before moving from 'results' to 'data' return result.get("result") def get_last_refresh(self, insight: Insight): if self.context["request"].GET.get("refresh"): return now() result = self.get_result(insight) if result is not None: return insight.last_refresh insight.last_refresh = None insight.save() return None def to_representation(self, instance: Insight): representation = super().to_representation(instance) representation["filters"] = instance.dashboard_filters( dashboard=self.context.get("dashboard")) return representation
class ExperimentSerializer(serializers.ModelSerializer): feature_flag_key = serializers.CharField(source="get_feature_flag_key") created_by = UserBasicSerializer(read_only=True) class Meta: model = Experiment fields = [ "id", "name", "description", "start_date", "end_date", "feature_flag_key", # get the FF id as well to link to FF UI "feature_flag", "parameters", "secondary_metrics", "filters", "archived", "created_by", "created_at", "updated_at", ] read_only_fields = [ "id", "created_by", "created_at", "updated_at", "feature_flag", ] def validate_parameters(self, value): if not value: return value variants = value.get("feature_flag_variants", []) if len(variants) > 4: raise ValidationError("Feature flag variants must be less than 5") elif len(variants) > 0: if "control" not in [variant["key"] for variant in variants]: raise ValidationError( "Feature flag variants must contain a control variant") return value def create(self, validated_data: dict, *args: Any, **kwargs: Any) -> Experiment: if not validated_data.get("filters"): raise ValidationError( "Filters are required to create an Experiment") variants = [] if validated_data["parameters"]: variants = validated_data["parameters"].get( "feature_flag_variants", []) request = self.context["request"] validated_data["created_by"] = request.user team = Team.objects.get(id=self.context["team_id"]) feature_flag_key = validated_data.pop("get_feature_flag_key") is_draft = "start_date" not in validated_data or validated_data[ "start_date"] is None properties = validated_data["filters"].get("properties", []) default_variants = [ { "key": "control", "name": "Control Group", "rollout_percentage": 50 }, { "key": "test", "name": "Test Variant", "rollout_percentage": 50 }, ] filters = { "groups": [{ "properties": properties, "rollout_percentage": None }], "multivariate": { "variants": variants or default_variants }, } if validated_data["filters"].get("aggregation_group_type_index"): filters["aggregation_group_type_index"] = validated_data[ "filters"]["aggregation_group_type_index"] feature_flag_serializer = FeatureFlagSerializer( data={ "key": feature_flag_key, "name": f'Feature Flag for Experiment {validated_data["name"]}', "filters": filters, "active": not is_draft, }, context=self.context, ) feature_flag_serializer.is_valid(raise_exception=True) feature_flag = feature_flag_serializer.save() experiment = Experiment.objects.create(team=team, feature_flag=feature_flag, **validated_data) return experiment def update(self, instance: Experiment, validated_data: dict, *args: Any, **kwargs: Any) -> Experiment: has_start_date = validated_data.get("start_date") is not None feature_flag = instance.feature_flag expected_keys = set([ "name", "description", "start_date", "end_date", "filters", "parameters", "archived", "secondary_metrics" ]) given_keys = set(validated_data.keys()) extra_keys = given_keys - expected_keys if feature_flag.key == validated_data.get("get_feature_flag_key"): extra_keys.remove("get_feature_flag_key") if extra_keys: raise ValidationError( f"Can't update keys: {', '.join(sorted(extra_keys))} on Experiment" ) if "feature_flag_variants" in validated_data.get("parameters", {}): if len(validated_data["parameters"] ["feature_flag_variants"]) != len(feature_flag.variants): raise ValidationError( "Can't update feature_flag_variants on Experiment") for variant in validated_data["parameters"][ "feature_flag_variants"]: if (len([ ff_variant for ff_variant in feature_flag.variants if ff_variant["key"] == variant["key"] and ff_variant["rollout_percentage"] == variant["rollout_percentage"] ]) != 1): raise ValidationError( "Can't update feature_flag_variants on Experiment") feature_flag_properties = validated_data.get("filters", {}).get("properties") if feature_flag_properties is not None: feature_flag.filters["groups"][0][ "properties"] = feature_flag_properties feature_flag.save() feature_flag_group_type_index = validated_data.get( "filters", {}).get("aggregation_group_type_index") # Only update the group type index when filters are sent if validated_data.get("filters"): feature_flag.filters[ "aggregation_group_type_index"] = feature_flag_group_type_index feature_flag.save() if instance.is_draft and has_start_date: feature_flag.active = True feature_flag.save() return super().update(instance, validated_data) elif has_start_date: raise ValidationError( "Can't change experiment start date after experiment has begun" ) else: # Not a draft, doesn't have start date # Or draft without start date return super().update(instance, validated_data)