diff --git a/ee/api/test/test_billing.py b/ee/api/test/test_billing.py index 6be27077cbeb2..0d0ed24847b4b 100644 --- a/ee/api/test/test_billing.py +++ b/ee/api/test/test_billing.py @@ -648,7 +648,7 @@ def test_license_is_updated_on_billing_load(self, mock_request): assert license.valid_until == datetime(2022, 1, 31, 12, 0, 0, tzinfo=ZoneInfo("UTC")) @patch("ee.api.billing.requests.get") - def test_organization_available_features_updated_if_different(self, mock_request): + def test_organization_available_product_features_updated_if_different(self, mock_request): def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock: mock = MagicMock() mock.status_code = 404 @@ -659,20 +659,31 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma elif "api/billing" in url: mock.status_code = 200 mock.json.return_value = create_billing_response( - customer=create_billing_customer(available_features=["feature1", "feature2"]) + customer=create_billing_customer( + available_product_features=[ + {"key": "feature1", "name": "feature1"}, + {"key": "feature2", "name": "feature2"}, + ] + ) ) return mock mock_request.side_effect = mock_implementation - self.organization.available_features = [] + self.organization.available_product_features = [] self.organization.save() - assert self.organization.available_features == [] + assert self.organization.available_product_features == [] self.client.get("/api/billing-v2") self.organization.refresh_from_db() - assert self.organization.available_features == ["feature1", "feature2"] + assert self.organization.available_product_features == [ + { + "key": "feature1", + "name": "feature1", + }, + {"key": "feature2", "name": "feature2"}, + ] @patch("ee.api.billing.requests.get") def test_organization_usage_update(self, mock_request): diff --git a/ee/billing/billing_manager.py b/ee/billing/billing_manager.py index 43c40498e279e..91d2f970ee3a6 100644 --- a/ee/billing/billing_manager.py +++ b/ee/billing/billing_manager.py @@ -344,11 +344,6 @@ def update_org_details(self, organization: Organization, billing_status: Billing org_modified = True sync_org_quota_limits(organization) - available_features = data.get("available_features", None) - if available_features and available_features != organization.available_features: - organization.available_features = data["available_features"] - org_modified = True - available_product_features = data.get("available_product_features", None) if available_product_features and available_product_features != organization.available_product_features: organization.available_product_features = data["available_product_features"] diff --git a/ee/billing/billing_types.py b/ee/billing/billing_types.py index f17365e20bd6e..512c66499f20f 100644 --- a/ee/billing/billing_types.py +++ b/ee/billing/billing_types.py @@ -81,12 +81,23 @@ class UsageSummary(TypedDict): usage: Optional[int] +class ProductFeature(TypedDict): + key: str + name: str + description: str + unit: Optional[str] + limit: Optional[int] + note: Optional[str] + is_plan_default: bool + + class CustomerInfo(TypedDict): customer_id: Optional[str] deactivated: bool has_active_subscription: bool billing_period: BillingPeriod available_features: list[AvailableFeature] + available_product_features: list[ProductFeature] current_total_amount_usd: Optional[str] current_total_amount_usd_after_discount: Optional[str] products: Optional[list[CustomerProduct]] @@ -103,16 +114,6 @@ class BillingStatus(TypedDict): customer: CustomerInfo -class ProductFeature(TypedDict): - key: str - name: str - description: str - unit: Optional[str] - limit: Optional[int] - note: Optional[str] - is_plan_default: bool - - class ProductPlan(TypedDict): """ A plan for a product that a customer can upgrade/downgrade to. diff --git a/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png b/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png index 5090a2518e09f..a604c0f86b375 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png and b/frontend/__snapshots__/scenes-app-batchexports--view-export--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-batchexports--view-export--light.png b/frontend/__snapshots__/scenes-app-batchexports--view-export--light.png index a616bb6b72c2f..7d436ec2f2068 100644 Binary files a/frontend/__snapshots__/scenes-app-batchexports--view-export--light.png and b/frontend/__snapshots__/scenes-app-batchexports--view-export--light.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png index b4d62210701db..14abf5b50f4d1 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png index 8bc28475e5c78..7e3160cf5efa2 100644 Binary files a/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png and b/frontend/__snapshots__/scenes-other-settings--settings-project-with-replay-features--light.png differ diff --git a/frontend/src/lib/api.mock.ts b/frontend/src/lib/api.mock.ts index fb5b230e42358..725f49ed5fda9 100644 --- a/frontend/src/lib/api.mock.ts +++ b/frontend/src/lib/api.mock.ts @@ -100,7 +100,6 @@ export const MOCK_DEFAULT_ORGANIZATION: OrganizationType = { plugins_access_level: PluginsAccessLevel.Root, enforce_2fa: false, teams: [MOCK_DEFAULT_TEAM], - available_features: [], is_member_join_email_enabled: true, metadata: {}, available_product_features: [], diff --git a/frontend/src/lib/components/Sharing/sharingLogic.ts b/frontend/src/lib/components/Sharing/sharingLogic.ts index fd6900c1414bf..abc2a53dc071d 100644 --- a/frontend/src/lib/components/Sharing/sharingLogic.ts +++ b/frontend/src/lib/components/Sharing/sharingLogic.ts @@ -96,8 +96,8 @@ export const sharingLogic = kea([ selectors({ siteUrl: [() => [preflightLogic.selectors.preflight], (preflight) => preflight?.site_url], whitelabelAvailable: [ - () => [userLogic.selectors.user], - (user) => (user?.organization?.available_features || []).includes(AvailableFeature.WHITE_LABELLING), + () => [userLogic.selectors.hasAvailableFeature], + (hasAvailableFeature) => hasAvailableFeature(AvailableFeature.WHITE_LABELLING), ], params: [ diff --git a/frontend/src/mocks/features.ts b/frontend/src/mocks/features.ts index 06137654115eb..10d4c23a7bb51 100644 --- a/frontend/src/mocks/features.ts +++ b/frontend/src/mocks/features.ts @@ -1,9 +1,14 @@ -import { AvailableFeature } from '~/types' +import { AvailableFeature, BillingV2FeatureType } from '~/types' let features: AvailableFeature[] = [] export const useAvailableFeatures = (f: AvailableFeature[]): void => { features = f } -export const getAvailableFeatures = (): AvailableFeature[] => { - return features +export const getAvailableProductFeatures = (): BillingV2FeatureType[] => { + return features.map((feature) => { + return { + key: feature, + name: feature, + } + }) } diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index e155aa67cf805..36d0272de0dc3 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -12,9 +12,9 @@ import { } from 'lib/api.mock' import { ResponseComposition, RestContext, RestRequest } from 'msw' -import { getAvailableFeatures } from '~/mocks/features' import { SharingConfigurationType } from '~/types' +import { getAvailableProductFeatures } from './features' import { billingJson } from './fixtures/_billing_v2' import { Mocks, MockSignature, mocksToHandlers } from './utils' @@ -77,7 +77,7 @@ export const defaultMocks: Mocks = { '/api/projects/:team_id/warehouse_tables/': EMPTY_PAGINATED_RESPONSE, '/api/organizations/@current/': (): MockSignature => [ 200, - { ...MOCK_DEFAULT_ORGANIZATION, available_features: getAvailableFeatures() }, + { ...MOCK_DEFAULT_ORGANIZATION, available_product_features: getAvailableProductFeatures() }, ], '/api/organizations/@current/roles/': EMPTY_PAGINATED_RESPONSE, '/api/organizations/@current/members/': toPaginatedResponse([ @@ -97,7 +97,10 @@ export const defaultMocks: Mocks = { 200, { ...MOCK_DEFAULT_USER, - organization: { ...MOCK_DEFAULT_ORGANIZATION, available_features: getAvailableFeatures() }, + organization: { + ...MOCK_DEFAULT_ORGANIZATION, + available_product_features: getAvailableProductFeatures(), + }, }, ], '/api/projects/@current/': MOCK_DEFAULT_TEAM, diff --git a/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx b/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx index 6fdabd2850fd7..e2ad4a230eece 100644 --- a/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx +++ b/frontend/src/queries/nodes/InsightViz/EditorFilters.tsx @@ -46,8 +46,7 @@ export interface EditorFiltersProps { } export function EditorFilters({ query, showing, embedded }: EditorFiltersProps): JSX.Element | null { - const { user } = useValues(userLogic) - const availableFeatures = user?.organization?.available_features || [] + const { hasAvailableFeature } = useValues(userLogic) const { insight, insightProps } = useValues(insightLogic) const { @@ -74,7 +73,7 @@ export function EditorFilters({ query, showing, embedded }: EditorFiltersProps): (isTrends && !NON_BREAKDOWN_DISPLAY_TYPES.includes(display || ChartDisplayType.ActionsLineGraph)) || isStepsFunnel || isTrendsFunnel - const hasPathsAdvanced = availableFeatures.includes(AvailableFeature.PATHS_ADVANCED) + const hasPathsAdvanced = hasAvailableFeature(AvailableFeature.PATHS_ADVANCED) const hasAttribution = isStepsFunnel const hasPathsHogQL = isPaths && pathsFilter?.includeEventTypes?.includes(PathType.HogQL) diff --git a/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx b/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx index 6dcd5a7677643..aa43412468c95 100644 --- a/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx +++ b/frontend/src/scenes/insights/views/Paths/PathStepPicker.tsx @@ -16,13 +16,12 @@ export function PathStepPicker(): JSX.Element { const { insightProps } = useValues(insightLogic) const { pathsFilter } = useValues(pathsDataLogic(insightProps)) const { updateInsightFilter } = useActions(pathsDataLogic(insightProps)) + const { hasAvailableFeature } = useValues(userLogic) const { stepLimit } = pathsFilter || {} - const { user } = useValues(userLogic) - const MIN = 2, - MAX = user?.organization?.available_features.includes(AvailableFeature.PATHS_ADVANCED) ? 20 : 5 + MAX = hasAvailableFeature(AvailableFeature.PATHS_ADVANCED) ? 20 : 5 const options: StepOption[] = Array.from(Array.from(Array.from(Array(MAX + 1).keys()).slice(MIN)), (v) => ({ label: `${v} Steps`, diff --git a/frontend/src/scenes/organizationLogic.test.ts b/frontend/src/scenes/organizationLogic.test.ts index 496f6f7307740..3ab247a0d6506 100644 --- a/frontend/src/scenes/organizationLogic.test.ts +++ b/frontend/src/scenes/organizationLogic.test.ts @@ -36,7 +36,7 @@ describe('organizationLogic', () => { it('loads organization from API', async () => { await expectLogic(logic).toDispatchActions(['loadCurrentOrganization', 'loadCurrentOrganizationSuccess']) await expectLogic(logic).toMatchValues({ - currentOrganization: { ...MOCK_DEFAULT_ORGANIZATION, available_features: [] }, + currentOrganization: { ...MOCK_DEFAULT_ORGANIZATION }, }) }) }) diff --git a/frontend/src/scenes/organizationLogic.tsx b/frontend/src/scenes/organizationLogic.tsx index 83edc751ba70a..5d2690d3eb0a7 100644 --- a/frontend/src/scenes/organizationLogic.tsx +++ b/frontend/src/scenes/organizationLogic.tsx @@ -1,4 +1,4 @@ -import { actions, afterMount, kea, listeners, path, reducers, selectors } from 'kea' +import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import api, { ApiConfig } from 'lib/api' import { OrganizationMembershipLevel } from 'lib/constants' @@ -22,6 +22,7 @@ export const organizationLogic = kea([ deleteOrganizationSuccess: true, deleteOrganizationFailure: true, }), + connect([userLogic]), reducers({ organizationBeingDeleted: [ null as OrganizationType | null, @@ -65,8 +66,8 @@ export const organizationLogic = kea([ })), selectors({ hasTagging: [ - (s) => [s.currentOrganization], - (currentOrganization) => currentOrganization?.available_features?.includes(AvailableFeature.TAGGING), + () => [userLogic.selectors.hasAvailableFeature], + (hasAvailableFeature) => hasAvailableFeature(AvailableFeature.TAGGING), ], isCurrentOrganizationUnavailable: [ (s) => [s.currentOrganization, s.currentOrganizationLoading], diff --git a/frontend/src/scenes/paths/PathNodeCardButton.tsx b/frontend/src/scenes/paths/PathNodeCardButton.tsx index 754a8c12937ec..7644501356646 100644 --- a/frontend/src/scenes/paths/PathNodeCardButton.tsx +++ b/frontend/src/scenes/paths/PathNodeCardButton.tsx @@ -30,8 +30,8 @@ export function PathNodeCardButton({ filter, setFilter, }: PathNodeCardButton): JSX.Element { - const { user } = useValues(userLogic) - const hasAdvancedPaths = user?.organization?.available_features?.includes(AvailableFeature.PATHS_ADVANCED) + const { hasAvailableFeature } = useValues(userLogic) + const hasAdvancedPaths = hasAvailableFeature(AvailableFeature.PATHS_ADVANCED) const nodeName = pageUrl(node) const isPath = nodeName.includes('/') diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap b/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap index af6b859c4c284..ef4f1eeffa83c 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/__snapshots__/verifiedDomainsLogic.test.ts.snap @@ -5,11 +5,16 @@ exports[`verifiedDomainsLogic values has proper defaults 1`] = ` "addModalShown": false, "configureSAMLModalId": null, "currentOrganization": { - "available_features": [ - "sso_enforcement", - "saml", + "available_product_features": [ + { + "key": "sso_enforcement", + "name": "sso_enforcement", + }, + { + "key": "saml", + "name": "saml", + }, ], - "available_product_features": [], "created_at": "2020-09-24T15:05:01.254111Z", "customer_id": null, "enforce_2fa": false, diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts index 6149a36e9f6d7..754ec294bfa8b 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.test.ts @@ -1,4 +1,5 @@ import { expectLogic } from 'kea-test-utils' +import { userLogic } from 'scenes/userLogic' import { useAvailableFeatures } from '~/mocks/features' import { useMocks } from '~/mocks/jest' @@ -9,6 +10,7 @@ import { isSecureURL, verifiedDomainsLogic } from './verifiedDomainsLogic' describe('verifiedDomainsLogic', () => { let logic: ReturnType + let userlogic: ReturnType beforeEach(() => { useAvailableFeatures([AvailableFeature.SSO_ENFORCEMENT, AvailableFeature.SAML]) @@ -54,6 +56,8 @@ describe('verifiedDomainsLogic', () => { }) initKeaTests() logic = verifiedDomainsLogic() + userlogic = userLogic() + userlogic.mount() logic.mount() }) @@ -81,6 +85,7 @@ describe('verifiedDomainsLogic', () => { describe('values', () => { it('has proper defaults', async () => { + await expectLogic(userlogic).toFinishAllListeners() await expectLogic(logic).toFinishAllListeners() expect(logic.values).toMatchSnapshot() }) diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts index 00778b3e82875..de997f5d7b1cc 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts @@ -5,6 +5,7 @@ import api from 'lib/api' import { SECURE_URL_REGEX } from 'lib/constants' import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast' import { organizationLogic } from 'scenes/organizationLogic' +import { userLogic } from 'scenes/userLogic' import { AvailableFeature, OrganizationDomainType } from '~/types' @@ -31,7 +32,7 @@ export const isSecureURL = (url: string): boolean => { export const verifiedDomainsLogic = kea([ path(['scenes', 'organization', 'verifiedDomainsLogic']), - connect({ values: [organizationLogic, ['currentOrganization']] }), + connect({ values: [organizationLogic, ['currentOrganization']], logic: [userLogic] }), actions({ replaceDomain: (domain: OrganizationDomainType) => ({ domain }), setAddModalShown: (shown: boolean) => ({ shown }), @@ -134,14 +135,12 @@ export const verifiedDomainsLogic = kea([ (verifyingId && verifiedDomains.find(({ id }) => id === verifyingId)) || null, ], isSSOEnforcementAvailable: [ - (s) => [s.currentOrganization], - (currentOrganization): boolean => - currentOrganization?.available_features.includes(AvailableFeature.SSO_ENFORCEMENT) ?? false, + () => [userLogic.selectors.hasAvailableFeature], + (hasAvailableFeature): boolean => hasAvailableFeature(AvailableFeature.SSO_ENFORCEMENT), ], isSAMLAvailable: [ - (s) => [s.currentOrganization], - (currentOrganization): boolean => - currentOrganization?.available_features.includes(AvailableFeature.SAML) ?? false, + () => [userLogic.selectors.hasAvailableFeature], + (hasAvailableFeature): boolean => hasAvailableFeature(AvailableFeature.SAML), ], }), afterMount(({ actions }) => actions.loadVerifiedDomains()), diff --git a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx index c18cc609e07af..f1803e750b7ac 100644 --- a/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx +++ b/frontend/src/scenes/settings/project/PathCleaningFiltersConfig.tsx @@ -10,8 +10,8 @@ import { AvailableFeature, InsightType } from '~/types' export function PathCleaningFiltersConfig(): JSX.Element | null { const { updateCurrentTeam } = useActions(teamLogic) const { currentTeam } = useValues(teamLogic) - const { user } = useValues(userLogic) - const hasAdvancedPaths = user?.organization?.available_features?.includes(AvailableFeature.PATHS_ADVANCED) + const { hasAvailableFeature } = useValues(userLogic) + const hasAdvancedPaths = hasAvailableFeature(AvailableFeature.PATHS_ADVANCED) if (!currentTeam) { return null diff --git a/frontend/src/scenes/userLogic.ts b/frontend/src/scenes/userLogic.ts index 8e8ccbe98ce5d..9db1e96fa8806 100644 --- a/frontend/src/scenes/userLogic.ts +++ b/frontend/src/scenes/userLogic.ts @@ -151,7 +151,7 @@ export const userLogic = kea([ name: user.organization.name, slug: user.organization.slug, created_at: user.organization.created_at, - available_features: user.organization.available_features, + available_product_features: user.organization.available_product_features, ...user.organization.metadata, }) @@ -214,8 +214,7 @@ export const userLogic = kea([ : true : false } - // if we don't have the new available_product_features obj, fallback to old available_features - return !!user?.organization?.available_features.includes(feature) + return false } }, ], diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b864eec21d68f..a23840aaaf6b9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -305,7 +305,6 @@ export interface OrganizationType extends OrganizationBasicType { updated_at: string plugins_access_level: PluginsAccessLevel teams: TeamBasicType[] - available_features: AvailableFeatureUnion[] available_product_features: BillingV2FeatureType[] is_member_join_email_enabled: boolean customer_id: string | null diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 1a1bd84e61bd8..7402bbd0cd0e9 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -181,8 +181,6 @@ posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item " posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "select" [union-attr] posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "group_by" [union-attr] posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item "None" of "list[Expr] | Any | None" has no attribute "append" [union-attr] -ee/billing/billing_manager.py:0: error: TypedDict "CustomerInfo" has no key "available_product_features" [typeddict-item] -ee/billing/billing_manager.py:0: note: Did you mean "available_features"? posthog/hogql/resolver.py:0: error: Argument 1 of "visit" is incompatible with supertype "Visitor"; supertype defines the argument type as "AST" [override] posthog/hogql/resolver.py:0: note: This violates the Liskov substitution principle posthog/hogql/resolver.py:0: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides diff --git a/plugin-server/functional_tests/api.ts b/plugin-server/functional_tests/api.ts index 2d7ab9a5db48d..85a52014d389d 100644 --- a/plugin-server/functional_tests/api.ts +++ b/plugin-server/functional_tests/api.ts @@ -350,8 +350,9 @@ export const createOrganization = async (organizationProperties = {}) => { personalization: '{}', // DEPRECATED setup_section_2_completed: true, // DEPRECATED for_internal_metrics: false, - available_features: [], domain_whitelist: [], + available_features: [], + available_product_features: [], is_member_join_email_enabled: false, slug: Math.round(Math.random() * 20000), ...organizationProperties, diff --git a/posthog/api/test/test_organization.py b/posthog/api/test/test_organization.py index 285a074e341cc..6bbfb16be03e6 100644 --- a/posthog/api/test/test_organization.py +++ b/posthog/api/test/test_organization.py @@ -1,8 +1,8 @@ +from asgiref.sync import sync_to_async from rest_framework import status from posthog.models import Organization, OrganizationMembership, Team from posthog.test.base import APIBaseTest -from asgiref.sync import sync_to_async class TestOrganizationAPI(APIBaseTest): diff --git a/posthog/api/user.py b/posthog/api/user.py index 2d59a92a52f5d..36161eac87ca0 100644 --- a/posthog/api/user.py +++ b/posthog/api/user.py @@ -1,12 +1,16 @@ import json import os import secrets -import structlog +import time import urllib.parse from base64 import b32encode from binascii import unhexlify +from datetime import datetime, timedelta from typing import Any, Optional, cast + +import jwt import requests +import structlog from django.conf import settings from django.contrib.auth import login, update_session_auth_hash from django.contrib.auth.password_validation import validate_password @@ -22,21 +26,22 @@ from rest_framework import exceptions, mixins, serializers, viewsets from rest_framework.decorators import action from rest_framework.exceptions import NotFound -from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response - from two_factor.forms import TOTPDeviceForm from two_factor.utils import default_device -import time -import jwt -from datetime import datetime, timedelta from posthog.api.decide import hostname_in_allowed_url_list from posthog.api.email_verification import EmailVerifier from posthog.api.organization import OrganizationSerializer from posthog.api.shared import OrganizationBasicSerializer, TeamBasicSerializer -from posthog.api.utils import raise_if_user_provided_url_unsafe, PublicIPOnlyHttpAdapter -from posthog.auth import PersonalAPIKeyAuthentication, SessionAuthentication, authenticate_secondarily +from posthog.api.utils import PublicIPOnlyHttpAdapter, raise_if_user_provided_url_unsafe +from posthog.auth import ( + PersonalAPIKeyAuthentication, + SessionAuthentication, + authenticate_secondarily, +) +from posthog.constants import PERMITTED_FORUM_DOMAINS from posthog.email import is_email_available from posthog.event_usage import ( report_user_logged_in, @@ -44,7 +49,7 @@ report_user_verified_email, ) from posthog.middleware import get_impersonated_session_expires_at -from posthog.models import Team, User, UserScenePersonalisation, Dashboard +from posthog.models import Dashboard, Team, User, UserScenePersonalisation from posthog.models.organization import Organization from posthog.models.user import NOTIFICATION_DEFAULTS, Notifications from posthog.permissions import APIScopePermission @@ -53,7 +58,6 @@ from posthog.tasks.email import send_email_change_emails from posthog.user_permissions import UserPermissions from posthog.utils import get_js_url -from posthog.constants import PERMITTED_FORUM_DOMAINS logger = structlog.get_logger(__name__) diff --git a/posthog/tasks/tasks.py b/posthog/tasks/tasks.py index d45ce43200511..5aa9816bbd785 100644 --- a/posthog/tasks/tasks.py +++ b/posthog/tasks/tasks.py @@ -7,6 +7,7 @@ from django.db import connection from django.utils import timezone from prometheus_client import Gauge +from structlog import get_logger from posthog.cloud_utils import is_cloud from posthog.errors import CHQueryErrorTooManySimultaneousQueries @@ -16,8 +17,6 @@ from posthog.redis import get_client from posthog.tasks.utils import CeleryQueue -from structlog import get_logger - logger = get_logger(__name__)