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
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)
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)
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)
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}
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()
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,)
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
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
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(), })
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, ), ),
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()
def async_migrations_emails_enabled(): return is_email_available() and not getattr( config, "ASYNC_MIGRATIONS_OPT_OUT_EMAILS")
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), }, })
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")), ]