예제 #1
0
    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()

        if not self.context.get("bulk_create"):
            report_team_member_invited(
                self.context["request"].user.distinct_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(),
                email_available=is_email_available(),
            )

        return invite
예제 #2
0
def preflight_check(request: HttpRequest) -> JsonResponse:
    response = {
        "django": True,
        "redis": is_redis_alive() or settings.TEST,
        "plugins": is_plugin_server_alive() or settings.TEST,
        "celery": is_celery_alive() or settings.TEST,
        "db": is_postgres_alive(),
        "initiated": Organization.objects.exists(),
        "cloud": settings.MULTI_TENANCY,
        "demo": settings.DEMO,
        "realm": get_instance_realm(),
        "available_social_auth_providers":
        get_available_social_auth_providers(),
        "can_create_org": get_can_create_org(),
        "email_service_available": is_email_available(with_absolute_urls=True),
    }

    if request.user.is_authenticated:
        response = {
            **response,
            "db_backend": settings.PRIMARY_DB.value,
            "available_timezones": get_available_timezones_with_offsets(),
            "opt_out_capture": os.environ.get("OPT_OUT_CAPTURE", False),
            "posthog_version": VERSION,
            "is_debug": settings.DEBUG,
            "is_event_property_usage_enabled":
            settings.ASYNC_EVENT_PROPERTY_USAGE,
            "licensed_users_available": get_licensed_users_available(),
            "site_url": settings.SITE_URL,
            "instance_preferences": settings.INSTANCE_PREFERENCES,
        }

    return JsonResponse(response)
예제 #3
0
파일: views.py 프로젝트: neilkakkar/posthog
def preflight_check(request: HttpRequest) -> JsonResponse:

    response = {
        "django": True,
        "redis": is_redis_alive() or settings.TEST,
        "plugins": is_plugin_server_alive() or settings.TEST,
        "celery": is_celery_alive() or settings.TEST,
        "db": is_postgres_alive(),
        "initiated": User.objects.exists() if not settings.E2E_TESTING else
        False,  # Enables E2E testing of signup flow
        "cloud": settings.MULTI_TENANCY,
        "available_social_auth_providers":
        get_available_social_auth_providers(),
    }

    if request.user.is_authenticated:
        response = {
            **response,
            "ee_available": settings.EE_AVAILABLE,
            "ee_enabled": is_ee_enabled(),
            "db_backend": settings.PRIMARY_DB.value,
            "available_timezones": get_available_timezones_with_offsets(),
            "opt_out_capture": os.environ.get("OPT_OUT_CAPTURE", False),
            "posthog_version": VERSION,
            "email_service_available":
            is_email_available(with_absolute_urls=True),
            "is_debug": settings.DEBUG,
            "is_event_property_usage_enabled":
            settings.ASYNC_EVENT_PROPERTY_USAGE,
            "licensed_users_available": get_licensed_users_available(),
            "site_url": settings.SITE_URL,
        }

    return JsonResponse(response)
예제 #4
0
    def bulk(self, request: request.Request, **kwargs) -> response.Response:
        data = cast(Any, request.data)
        if not isinstance(data, list):
            raise exceptions.ValidationError("This endpoint needs an array of data for bulk invite creation.")
        if len(data) > 20:
            raise exceptions.ValidationError(
                "A maximum of 20 invites can be sent in a single request.", code="max_length",
            )

        serializer = OrganizationInviteSerializer(
            data=data, many=True, context={**self.get_serializer_context(), "bulk_create": True}
        )
        serializer.is_valid(raise_exception=True)
        serializer.save()

        organization = Organization.objects.get(id=self.organization_id)
        report_bulk_invited(
            cast(User, self.request.user),
            invitee_count=len(serializer.validated_data),
            name_count=sum(1 for invite in serializer.validated_data if invite.get("first_name")),
            current_invite_count=organization.active_invites.count(),
            current_member_count=organization.memberships.count(),
            email_available=is_email_available(),
        )

        return response.Response(serializer.data, status=status.HTTP_201_CREATED)
예제 #5
0
    def create(self, validated_data: Dict[str, Any]) -> Dict[str, Any]:
        output = []
        organization = Organization.objects.get(
            id=self.context["organization_id"])

        with transaction.atomic():
            for invite in validated_data["invites"]:
                self.context["bulk_create"] = True
                serializer = OrganizationInviteSerializer(data=invite,
                                                          context=self.context)
                serializer.is_valid(raise_exception=False
                                    )  # Don't raise, already validated before
                output.append(serializer.save())

        report_bulk_invited(
            self.context["request"].user.distinct_id,
            invitee_count=len(validated_data["invites"]),
            name_count=sum(1 for invite in validated_data["invites"]
                           if invite["first_name"]),
            current_invite_count=organization.active_invites.count(),
            current_member_count=organization.memberships.count(),
            email_available=is_email_available(),
        )

        return {"invites": output}
예제 #6
0
    def use(self, user: Any, *, prevalidated: bool = False) -> None:
        if not prevalidated:
            self.validate(user=user)
        user.join(organization=self.organization)
        if is_email_available(with_absolute_urls=True):
            from posthog.tasks.email import send_member_join

            send_member_join.apply_async(kwargs={"invitee_uuid": user.uuid, "organization_id": self.organization.id})
        OrganizationInvite.objects.filter(target_email__iexact=self.target_email).delete()
예제 #7
0
def send_weekly_email_reports() -> None:
    """
    Schedules an async task to send the weekly email report for each team.
    """

    if not is_email_available():
        logger.info("Skipping send_weekly_email_report because email is not properly configured")
        return

    for team in Team.objects.all():
        _send_weekly_email_report_for_team.delay(team_id=team.pk,)
예제 #8
0
    def create(self, validated_data):

        if getattr(settings, "SAML_ENFORCED", False):
            raise serializers.ValidationError(
                "Password reset is disabled because SAML login is enforced.",
                code="saml_enforced")

        if not is_email_available():
            raise serializers.ValidationError(
                "Cannot reset passwords because email is not configured for your instance. Please contact your administrator.",
                code="email_not_available",
            )

        email = validated_data.pop("email")
        try:
            user = User.objects.get(email=email)
        except User.DoesNotExist:
            user = None

        if user:
            token = default_token_generator.make_token(user)

            message = EmailMessage(
                campaign_key=f"password-reset-{user.uuid}-{timezone.now()}",
                subject=f"Reset your PostHog password",
                template_name="password_reset",
                template_context={
                    "preheader":
                    "Please follow the link inside to reset your password.",
                    "link":
                    f"/reset/{user.uuid}/{token}",
                    "cloud":
                    settings.MULTI_TENANCY,
                    "site_url":
                    settings.SITE_URL,
                    "social_providers":
                    list(user.social_auth.values_list("provider", flat=True)),
                },
            )
            message.add_recipient(email)
            message.send()

        # TODO: Limit number of requests for password reset emails

        return True
예제 #9
0
 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,
         target_email=validated_data["target_email"],
     )
     if is_email_available(with_absolute_urls=True):
         invite.emailing_attempt_made = True
         send_invite.delay(invite_id=invite.id)
         invite.save()
     return invite
예제 #10
0
def user(request):
    """
    DEPRECATED: This endpoint (/api/user/) has been deprecated in favor of /api/users/@me/
    and will be removed soon.
    """
    organization: Optional[Organization] = request.user.organization
    organizations = list(
        request.user.organizations.order_by("-created_at").values(
            "name", "id"))
    team: Optional[Team] = request.user.team
    teams = list(
        request.user.teams.order_by("-created_at").values("name", "id"))
    user = cast(User, request.user)

    if request.method == "PATCH":
        data = json.loads(request.body)

        if team is not None and "team" in data:
            team.app_urls = data["team"].get("app_urls", team.app_urls)
            team.slack_incoming_webhook = data["team"].get(
                "slack_incoming_webhook", team.slack_incoming_webhook)
            team.anonymize_ips = data["team"].get("anonymize_ips",
                                                  team.anonymize_ips)
            team.session_recording_opt_in = data["team"].get(
                "session_recording_opt_in", team.session_recording_opt_in)
            team.session_recording_retention_period_days = data["team"].get(
                "session_recording_retention_period_days",
                team.session_recording_retention_period_days,
            )
            team.completed_snippet_onboarding = data["team"].get(
                "completed_snippet_onboarding",
                team.completed_snippet_onboarding,
            )
            team.test_account_filters = data["team"].get(
                "test_account_filters", team.test_account_filters)
            team.timezone = data["team"].get("timezone", team.timezone)
            team.save()

        if "user" in data:
            try:
                user.current_organization = user.organizations.get(
                    id=data["user"]["current_organization_id"])
                assert user.organization is not None, "Organization should have been just set"
                user.current_team = user.organization.teams.first()
            except (KeyError, ValueError):
                pass
            except ObjectDoesNotExist:
                return JsonResponse(
                    {"detail": "Organization not found for user."}, status=404)
            except KeyError:
                pass
            except ObjectDoesNotExist:
                return JsonResponse(
                    {"detail": "Organization not found for user."}, status=404)
            if user.organization is not None:
                try:
                    user.current_team = user.organization.teams.get(
                        id=int(data["user"]["current_team_id"]))
                except (KeyError, TypeError):
                    pass
                except ValueError:
                    return JsonResponse(
                        {"detail": "Team ID must be an integer."}, status=400)
                except ObjectDoesNotExist:
                    return JsonResponse(
                        {
                            "detail":
                            "Team not found for user's current organization."
                        },
                        status=404)
            user.email_opt_in = data["user"].get("email_opt_in",
                                                 user.email_opt_in)
            user.anonymize_data = data["user"].get("anonymize_data",
                                                   user.anonymize_data)
            user.toolbar_mode = data["user"].get("toolbar_mode",
                                                 user.toolbar_mode)
            user.save()

    user_identify.identify_task.delay(user_id=user.id)

    return JsonResponse({
        "deprecation":
        "Endpoint has been deprecated. Please use `/api/users/@me/`.",
        "id":
        user.pk,
        "distinct_id":
        user.distinct_id,
        "name":
        user.first_name,
        "email":
        user.email,
        "email_opt_in":
        user.email_opt_in,
        "anonymize_data":
        user.anonymize_data,
        "toolbar_mode":
        user.toolbar_mode,
        "organization":
        None if organization is None else {
            "id":
            organization.id,
            "name":
            organization.name,
            "billing_plan":
            organization.billing_plan,
            "available_features":
            organization.available_features,
            "plugins_access_level":
            organization.plugins_access_level,
            "created_at":
            organization.created_at,
            "updated_at":
            organization.updated_at,
            "teams": [{
                "id": team.id,
                "name": team.name
            } for team in organization.teams.all().only("id", "name")],
        },
        "organizations":
        organizations,
        "team":
        None if team is None else {
            "id": team.id,
            "name": team.name,
            "app_urls": team.app_urls,
            "api_token": team.api_token,
            "anonymize_ips": team.anonymize_ips,
            "slack_incoming_webhook": team.slack_incoming_webhook,
            "completed_snippet_onboarding": team.completed_snippet_onboarding,
            "session_recording_opt_in": team.session_recording_opt_in,
            "session_recording_retention_period_days":
            team.session_recording_retention_period_days,
            "ingested_event": team.ingested_event,
            "is_demo": team.is_demo,
            "test_account_filters": team.test_account_filters,
            "timezone": team.timezone,
            "data_attributes": team.data_attributes,
        },
        "teams":
        teams,
        "has_password":
        user.has_usable_password(),
        "opt_out_capture":
        os.environ.get("OPT_OUT_CAPTURE"),
        "posthog_version":
        VERSION,
        "is_multi_tenancy":
        getattr(settings, "MULTI_TENANCY", False),
        "ee_available":
        settings.EE_AVAILABLE,
        "is_clickhouse_enabled":
        is_clickhouse_enabled(),
        "email_service_available":
        is_email_available(with_absolute_urls=True),
        "is_debug":
        getattr(settings, "DEBUG", False),
        "is_staff":
        user.is_staff,
        "is_impersonated":
        is_impersonated_session(request),
        "is_event_property_usage_enabled":
        getattr(settings, "ASYNC_EVENT_PROPERTY_USAGE", False),
        "realm":
        get_instance_realm(),
    })
예제 #11
0
 path("authorize_and_redirect/", login_required(authorize_and_redirect)),
 path("shared_dashboard/<str:share_token>", dashboard.shared_dashboard),
 re_path(r"^demo.*", login_required(demo)),
 # ingestion
 opt_slash_path("decide", decide.get_decide),
 opt_slash_path("e", capture.get_event),
 opt_slash_path("engage", capture.get_event),
 opt_slash_path("track", capture.get_event),
 opt_slash_path("capture", capture.get_event),
 opt_slash_path("batch", capture.get_event),
 opt_slash_path("s", capture.get_event),  # session recordings
 # auth
 path("logout", logout, name="login"),
 path("signup/finish/", signup.finish_social_signup, name="signup_finish"),
 path("", include("social_django.urls", namespace="social")),
 *([] if is_email_available() else [
     path(
         "accounts/password_reset/",
         TemplateView.as_view(
             template_name="registration/password_no_smtp.html"),
     )
 ]),
 path(
     "accounts/reset/<uidb64>/<token>/",
     auth_views.PasswordResetConfirmView.as_view(
         success_url="/",
         post_reset_login_backend=
         "django.contrib.auth.backends.ModelBackend",
         post_reset_login=True,
     ),
 ),
예제 #12
0
def send_weekly_email_report() -> None:
    """
    Sends the weekly email report to all users in a team.
    """

    if not is_email_available():
        logger.info(
            "Skipping send_weekly_email_report because email is not properly configured"
        )
        return

    period_start, period_end = get_previous_week()

    last_week_start: datetime.datetime = period_start - datetime.timedelta(7)
    last_week_end: datetime.datetime = period_end - datetime.timedelta(7)

    for team in Team.objects.all():

        event_data_set = Event.objects.filter(
            team=team,
            timestamp__gte=period_start,
            timestamp__lte=period_end,
        )

        active_users = PersonDistinctId.objects.filter(
            distinct_id__in=event_data_set.values(
                "distinct_id").distinct(), ).distinct()
        active_users_count: int = active_users.count()

        if active_users_count == 0:
            # TODO: Send an email prompting fix to no active users
            continue

        last_week_users = PersonDistinctId.objects.filter(
            distinct_id__in=Event.objects.filter(
                team=team,
                timestamp__gte=last_week_start,
                timestamp__lte=last_week_end,
            ).values("distinct_id").distinct(), ).distinct()
        last_week_users_count: int = last_week_users.count()

        two_weeks_ago_users = PersonDistinctId.objects.filter(
            distinct_id__in=Event.objects.filter(
                team=team,
                timestamp__gte=last_week_start - datetime.timedelta(7),
                timestamp__lte=last_week_end - datetime.timedelta(7),
            ).values("distinct_id").distinct(),
        ).distinct()  # used to compute delta in churned users
        two_weeks_ago_users_count: int = two_weeks_ago_users.count()

        not_last_week_users = PersonDistinctId.objects.filter(
            pk__in=active_users.difference(last_week_users, ).values_list(
                "pk",
                flat=True,
            ))  # users that were present this week but not last week

        churned_count = last_week_users.difference(active_users).count()
        churned_ratio: Optional[float] = (churned_count /
                                          last_week_users_count if
                                          last_week_users_count > 0 else None)
        last_week_churn_ratio: Optional[float] = (
            two_weeks_ago_users.difference(last_week_users).count() /
            two_weeks_ago_users_count
            if two_weeks_ago_users_count > 0 else None)
        churned_delta: Optional[float] = (
            churned_ratio / last_week_churn_ratio -
            1 if last_week_churn_ratio else None  # type: ignore
        )

        message = EmailMessage(
            f"PostHog weekly report for {period_start.strftime('%b %d, %Y')} to {period_end.strftime('%b %d')}",
            "weekly_report",
            {
                "preheader":
                f"Your PostHog weekly report is ready! Your team had {compact_number(active_users_count)} active users last week! 🎉",
                "team":
                team.name,
                "period_start":
                period_start,
                "period_end":
                period_end,
                "active_users":
                active_users_count,
                "active_users_delta":
                active_users_count / last_week_users_count -
                1 if last_week_users_count > 0 else None,
                "user_distribution": {
                    "new":
                    not_last_week_users.filter(
                        person__created_at__gte=period_start).count() /
                    active_users_count,
                    "retained":
                    active_users.intersection(last_week_users).count() /
                    active_users_count,
                    "resurrected":
                    not_last_week_users.filter(
                        person__created_at__lt=period_start).count() /
                    active_users_count,
                },
                "churned_users": {
                    "abs": churned_count,
                    "ratio": churned_ratio,
                    "delta": churned_delta
                },
            },
        )

        for user in team.organization.members.all():
            # TODO: Skip "unsubscribed" users
            message.add_recipient(user.email, user.first_name)

        # TODO: Schedule retry on failed attempt
        message.send()
예제 #13
0
def async_migrations_emails_enabled():
    return is_email_available() and not getattr(
        config, "ASYNC_MIGRATIONS_OPT_OUT_EMAILS")
예제 #14
0
def user(request):
    organization: Optional[Organization] = request.user.organization
    organizations = list(
        request.user.organizations.order_by("-created_at").values(
            "name", "id"))
    team: Optional[Team] = request.user.team
    teams = list(
        request.user.teams.order_by("-created_at").values("name", "id"))
    user = cast(User, request.user)

    if request.method == "PATCH":
        data = json.loads(request.body)

        if team is not None and "team" in data:
            team.app_urls = data["team"].get("app_urls", team.app_urls)
            team.opt_out_capture = data["team"].get("opt_out_capture",
                                                    team.opt_out_capture)
            team.slack_incoming_webhook = data["team"].get(
                "slack_incoming_webhook", team.slack_incoming_webhook)
            team.anonymize_ips = data["team"].get("anonymize_ips",
                                                  team.anonymize_ips)
            team.session_recording_opt_in = data["team"].get(
                "session_recording_opt_in", team.session_recording_opt_in)
            team.session_recording_retention_period_days = data["team"].get(
                "session_recording_retention_period_days",
                team.session_recording_retention_period_days)
            if data["team"].get("plugins_opt_in") is not None:
                reload_plugins_on_workers()
            team.plugins_opt_in = data["team"].get("plugins_opt_in",
                                                   team.plugins_opt_in)
            team.completed_snippet_onboarding = data["team"].get(
                "completed_snippet_onboarding",
                team.completed_snippet_onboarding,
            )
            team.save()

        if "user" in data:
            try:
                user.current_organization = user.organizations.get(
                    id=data["user"]["current_organization_id"])
                assert user.organization is not None, "Organization should have been just set"
                user.current_team = user.organization.teams.first()
            except (KeyError, ValueError):
                pass
            except ObjectDoesNotExist:
                return JsonResponse(
                    {"detail": "Organization not found for user."}, status=404)
            except KeyError:
                pass
            except ObjectDoesNotExist:
                return JsonResponse(
                    {"detail": "Organization not found for user."}, status=404)
            if user.organization is not None:
                try:
                    user.current_team = user.organization.teams.get(
                        id=int(data["user"]["current_team_id"]))
                except (KeyError, TypeError):
                    pass
                except ValueError:
                    return JsonResponse(
                        {"detail": "Team ID must be an integer."}, status=400)
                except ObjectDoesNotExist:
                    return JsonResponse(
                        {
                            "detail":
                            "Team not found for user's current organization."
                        },
                        status=404)
            user.email_opt_in = data["user"].get("email_opt_in",
                                                 user.email_opt_in)
            user.anonymize_data = data["user"].get("anonymize_data",
                                                   user.anonymize_data)
            user.toolbar_mode = data["user"].get("toolbar_mode",
                                                 user.toolbar_mode)
            user.save()

    user_identify.identify_task.delay(user_id=user.id)

    return JsonResponse({
        "id":
        user.pk,
        "distinct_id":
        user.distinct_id,
        "name":
        user.first_name,
        "email":
        user.email,
        "email_opt_in":
        user.email_opt_in,
        "anonymize_data":
        user.anonymize_data,
        "toolbar_mode":
        user.toolbar_mode,
        "organization":
        None if organization is None else {
            "id":
            organization.id,
            "name":
            organization.name,
            "billing_plan":
            organization.billing_plan,
            "available_features":
            organization.available_features,
            "created_at":
            organization.created_at,
            "updated_at":
            organization.updated_at,
            "teams": [{
                "id": team.id,
                "name": team.name
            } for team in organization.teams.all().only("id", "name")],
        },
        "organizations":
        organizations,
        "team":
        None if team is None else {
            "id":
            team.id,
            "name":
            team.name,
            "app_urls":
            team.app_urls,
            "api_token":
            team.api_token,
            "opt_out_capture":
            team.opt_out_capture,
            "anonymize_ips":
            team.anonymize_ips,
            "slack_incoming_webhook":
            team.slack_incoming_webhook,
            "event_names":
            team.event_names,
            "event_names_with_usage":
            team.event_names_with_usage or [{
                "event": event,
                "volume": None,
                "usage_count": None
            } for event in team.event_names],
            "event_properties":
            team.event_properties,
            "event_properties_numerical":
            team.event_properties_numerical,
            "event_properties_with_usage":
            team.event_properties_with_usage or [{
                "key": key,
                "volume": None,
                "usage_count": None
            } for key in team.event_properties],
            "completed_snippet_onboarding":
            team.completed_snippet_onboarding,
            "session_recording_opt_in":
            team.session_recording_opt_in,
            "session_recording_retention_period_days":
            team.session_recording_retention_period_days,
            "plugins_opt_in":
            team.plugins_opt_in,
            "ingested_event":
            team.ingested_event,
        },
        "teams":
        teams,
        "has_password":
        user.has_usable_password(),
        "opt_out_capture":
        os.environ.get("OPT_OUT_CAPTURE"),
        "posthog_version":
        VERSION,
        "is_multi_tenancy":
        getattr(settings, "MULTI_TENANCY", False),
        "ee_available":
        settings.EE_AVAILABLE,
        "ee_enabled":
        is_ee_enabled(),
        "email_service_available":
        is_email_available(with_absolute_urls=True),
        "is_debug":
        getattr(settings, "DEBUG", False),
        "is_staff":
        user.is_staff,
        "is_impersonated":
        is_impersonated_session(request),
        "plugin_access": {
            "install": can_install_plugins_via_api(user.organization),
            "configure": can_configure_plugins_via_api(user.organization),
        },
    })
예제 #15
0
    opt_slash_path("decide", decide.get_decide),
    opt_slash_path("e", capture.get_event),
    opt_slash_path("engage", capture.get_event),
    opt_slash_path("track", capture.get_event),
    opt_slash_path("capture", capture.get_event),
    opt_slash_path("batch", capture.get_event),
    opt_slash_path("s", capture.get_event),  # session recordings
    # auth
    path("logout", logout, name="login"),
    path("login", login_view, name="login"),
    path("signup/finish/", finish_social_signup, name="signup_finish"),
    path("signup/<str:invite_id>", signup_to_organization_view, name="signup"),
    path("", include("social_django.urls", namespace="social")),
    *(
        []
        if is_email_available()
        else [
            path("accounts/password_reset/", TemplateView.as_view(template_name="registration/password_no_smtp.html"),)
        ]
    ),
    path(
        "accounts/reset/<uidb64>/<token>/",
        auth_views.PasswordResetConfirmView.as_view(
            success_url="/",
            post_reset_login_backend="django.contrib.auth.backends.ModelBackend",
            post_reset_login=True,
        ),
    ),
    path("accounts/", include("django.contrib.auth.urls")),
]