-
- {t('Check your email ##UNVERIFIED_EMAIL##. ').replace(
- '##UNVERIFIED_EMAIL##',
- unverifiedEmail.email
- )}
-
-
- {t(
- 'A verification link has been sent to confirm your ownership. Once confirmed, this address will replace ##UNVERIFIED_EMAIL##'
- ).replace('##UNVERIFIED_EMAIL##', currentAccount.email)}
-
-
-
-
-
+
+
+
+
+
+
+ {email.refreshedEmail && (
+
+ )}
+ >
+ )}
-
-
+ {userCanChangeEmail && (
+
+
+
+ )}
);
}
diff --git a/jsapp/js/account/security/email/emailSection.module.scss b/jsapp/js/account/security/email/emailSection.module.scss
index 7675c0999f..3350c95c9d 100644
--- a/jsapp/js/account/security/email/emailSection.module.scss
+++ b/jsapp/js/account/security/email/emailSection.module.scss
@@ -34,3 +34,13 @@
display: flex;
gap: 10px;
}
+
+.emailText {
+ font-weight: 600;
+}
+
+.emailUpdateDisabled {
+ flex: 5;
+ // To compensate for the `options` class not displaying when there is no email
+ margin-right: calc(30% + 8px);
+}
diff --git a/jsapp/js/account/subscriptionStore.ts b/jsapp/js/account/subscriptionStore.ts
index be7469003f..ac9de4411c 100644
--- a/jsapp/js/account/subscriptionStore.ts
+++ b/jsapp/js/account/subscriptionStore.ts
@@ -1,9 +1,8 @@
import {makeAutoObservable} from 'mobx';
-import {handleApiFail} from 'js/api';
+import {handleApiFail, fetchGet} from 'js/api';
import {ACTIVE_STRIPE_STATUSES, ROOT_URL} from 'js/constants';
-import {fetchGet} from 'jsapp/js/api';
import type {PaginatedResponse} from 'js/dataInterface';
-import {Product, SubscriptionInfo} from 'js/account/stripe.types';
+import type {Product, SubscriptionInfo} from 'js/account/stripe.types';
const PRODUCTS_URL = '/api/v2/stripe/products/';
@@ -53,16 +52,16 @@ class SubscriptionStore {
);
this.canceledPlans = response.results.filter(
(sub) =>
- sub.items[0]?.price.product.metadata?.product_type == 'plan' &&
+ sub.items[0]?.price.product.metadata?.product_type === 'plan' &&
sub.status === 'canceled'
);
// get any active plan subscriptions for the user
this.planResponse = this.activeSubscriptions.filter(
- (sub) => sub.items[0]?.price.product.metadata?.product_type == 'plan'
+ (sub) => sub.items[0]?.price.product.metadata?.product_type === 'plan'
);
// get any active recurring add-on subscriptions for the user
this.addOnsResponse = this.activeSubscriptions.filter(
- (sub) => sub.items[0]?.price.product.metadata?.product_type == 'addon'
+ (sub) => sub.items[0]?.price.product.metadata?.product_type === 'addon'
);
this.isPending = false;
diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.component.tsx b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.component.tsx
similarity index 100%
rename from jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.component.tsx
rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.component.tsx
diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.module.scss b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.module.scss
similarity index 100%
rename from jsapp/js/account/usage/one-time-add-on-usage-modal/one-time-add-on-list/oneTimeAddOnList.module.scss
rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnList/oneTimeAddOnList.module.scss
diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component.tsx b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component.tsx
similarity index 97%
rename from jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component.tsx
rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component.tsx
index a347149937..c1e32895e6 100644
--- a/jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component.tsx
+++ b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component.tsx
@@ -4,7 +4,7 @@ import KoboModalHeader from 'js/components/modals/koboModalHeader';
import styles from './oneTimeAddOnUsageModal.module.scss';
import {OneTimeAddOn, RecurringInterval, USAGE_TYPE} from '../../stripe.types';
import {useLimitDisplay} from '../../stripe.utils';
-import OneTimeAddOnList from './one-time-add-on-list/oneTimeAddOnList.component';
+import OneTimeAddOnList from './oneTimeAddOnList/oneTimeAddOnList.component';
interface OneTimeAddOnUsageModalProps {
type: USAGE_TYPE;
diff --git a/jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.module.scss b/jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.module.scss
similarity index 100%
rename from jsapp/js/account/usage/one-time-add-on-usage-modal/oneTimeAddOnUsageModal.module.scss
rename to jsapp/js/account/usage/oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.module.scss
diff --git a/jsapp/js/account/usage/usageContainer.tsx b/jsapp/js/account/usage/usageContainer.tsx
index 6768dfaf30..40bca8edc5 100644
--- a/jsapp/js/account/usage/usageContainer.tsx
+++ b/jsapp/js/account/usage/usageContainer.tsx
@@ -13,7 +13,7 @@ import cx from 'classnames';
import subscriptionStore from 'js/account/subscriptionStore';
import Badge from 'js/components/common/badge';
import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
-import OneTimeAddOnUsageModal from './one-time-add-on-usage-modal/oneTimeAddOnUsageModal.component';
+import OneTimeAddOnUsageModal from './oneTimeAddOnUsageModal/oneTimeAddOnUsageModal.component';
interface UsageContainerProps {
usage: number;
diff --git a/kobo/apps/organizations/serializers.py b/kobo/apps/organizations/serializers.py
index df855b011a..ae13041394 100644
--- a/kobo/apps/organizations/serializers.py
+++ b/kobo/apps/organizations/serializers.py
@@ -29,7 +29,9 @@ class OrganizationUserSerializer(serializers.ModelSerializer):
source='user.date_joined', format='%Y-%m-%dT%H:%M:%SZ'
)
user__username = serializers.ReadOnlyField(source='user.username')
- user__name = serializers.ReadOnlyField(source='user.get_full_name')
+ user__extra_details__name = serializers.ReadOnlyField(
+ source='user.extra_details.data.name'
+ )
user__email = serializers.ReadOnlyField(source='user.email')
user__is_active = serializers.ReadOnlyField(source='user.is_active')
@@ -40,7 +42,7 @@ class Meta:
'user',
'user__username',
'user__email',
- 'user__name',
+ 'user__extra_details__name',
'role',
'user__has_mfa_enabled',
'date_joined',
diff --git a/kobo/apps/organizations/tests/test_organizations_api.py b/kobo/apps/organizations/tests/test_organizations_api.py
index b1e467d5e9..a3fa635543 100644
--- a/kobo/apps/organizations/tests/test_organizations_api.py
+++ b/kobo/apps/organizations/tests/test_organizations_api.py
@@ -61,7 +61,12 @@ def test_anonymous_user(self):
def test_create(self):
data = {'name': 'my org'}
res = self.client.post(self.url_list, data)
- self.assertContains(res, data['name'], status_code=201)
+ self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
+
+ def test_delete(self):
+ self._insert_data()
+ res = self.client.delete(self.url_detail)
+ self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
def test_list(self):
self._insert_data()
diff --git a/kobo/apps/organizations/views.py b/kobo/apps/organizations/views.py
index 343682f39a..7fd0e59470 100644
--- a/kobo/apps/organizations/views.py
+++ b/kobo/apps/organizations/views.py
@@ -22,7 +22,6 @@
from kpi.constants import ASSET_TYPE_SURVEY
from kpi.filters import AssetOrderingFilter, SearchFilter
from kpi.models.asset import Asset
-from kpi.paginators import AssetUsagePagination, OrganizationMembersPagination
from kpi.serializers.v2.service_usage import (
CustomAssetUsageSerializer,
ServiceUsageSerializer,
@@ -87,7 +86,7 @@ class OrganizationViewSet(viewsets.ModelViewSet):
serializer_class = OrganizationSerializer
lookup_field = 'id'
permission_classes = [HasOrgRolePermission]
- pagination_class = AssetUsagePagination
+ http_method_names = ['get', 'patch']
@action(
detail=True, methods=['GET'], permission_classes=[IsOrgAdminPermission]
@@ -399,7 +398,7 @@ class OrganizationMemberViewSet(viewsets.ModelViewSet):
### Remove Member
- Delete an organization member and their associated user account.
+ Delete an organization member.
DELETE /api/v2/organizations/{organization_id}/members/{username}/
@@ -422,7 +421,6 @@ class OrganizationMemberViewSet(viewsets.ModelViewSet):
"""
serializer_class = OrganizationUserSerializer
permission_classes = [HasOrgRolePermission]
- pagination_class = OrganizationMembersPagination
http_method_names = ['get', 'patch', 'delete']
lookup_field = 'user__username'
@@ -444,7 +442,7 @@ def get_queryset(self):
# Annotate with role based on organization ownership and admin status
queryset = OrganizationUser.objects.filter(
organization_id=organization_id
- ).annotate(
+ ).select_related('user__extra_details').annotate(
role=Case(
When(Exists(owner_subquery), then=Value('owner')),
When(is_admin=True, then=Value('admin')),
@@ -454,13 +452,3 @@ def get_queryset(self):
has_mfa_enabled=Exists(mfa_subquery)
)
return queryset
-
- def destroy(self, request, *args, **kwargs):
- """
- Delete an organization member and their associated user account
- """
- instance = self.get_object()
- user = instance.user
- instance.delete()
- user.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/tsconfig.json b/tsconfig.json
index b9f6f06ade..01e7baeb20 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,6 +10,7 @@
},
"target": "es2017",
"module": "es2020",
+ "lib": ["ES2021"],
"esModuleInterop": true,
"moduleResolution": "node",
"strict": true,