Пример #1
0
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
Пример #3
0
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
Пример #4
0
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)
Пример #5
0
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
Пример #6
0
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
Пример #7
0
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
Пример #8
0
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.")
Пример #9
0
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
Пример #10
0
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
Пример #11
0
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
Пример #12
0
 def to_representation(self, instance):
     serializer = UserBasicSerializer(instance=instance)
     return serializer.data
Пример #13
0
 def to_representation(self, instance) -> Dict:
     data = UserBasicSerializer(instance=instance).data
     data["redirect_url"] = "/ingestion" if not settings.DEMO else "/"
     return data
Пример #14
0
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
Пример #16
0
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"}
Пример #17
0
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
Пример #18
0
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
Пример #19
0
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
Пример #20
0
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
Пример #21
0
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
Пример #22
0
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"}
Пример #23
0
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
Пример #24
0
 def to_representation(self, instance) -> Dict:
     data = UserBasicSerializer(instance=instance).data
     data[
         "redirect_url"] = "/personalization" if self.enable_new_onboarding(
         ) else "/ingestion"
     return data
Пример #25
0
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")
Пример #26
0
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
Пример #27
0
 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
Пример #28
0
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
Пример #29
0
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
Пример #30
0
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)