Skip to content

Commit a4abe65

Browse files
Merge branch 'leszek/cleanup-view-switcher-etc' of github.com:kobotoolbox/kpi into leszek/cleanup-view-switcher-etc
2 parents 4f26cc5 + 410c046 commit a4abe65

31 files changed

+1059
-563
lines changed

dependencies/pip/dev_requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ oauthlib==3.2.2
414414
# -r dependencies/pip/requirements.in
415415
# django-oauth-toolkit
416416
# requests-oauthlib
417-
openpyxl==3.0.9
417+
openpyxl==3.1.3
418418
# via
419419
# -r dependencies/pip/requirements.in
420420
# pyxform
@@ -537,7 +537,7 @@ pytz==2024.1
537537
# via
538538
# flower
539539
# pandas
540-
pyxform==1.9.0
540+
pyxform==2.1.1
541541
# via
542542
# -r dependencies/pip/requirements.in
543543
# formpack

dependencies/pip/requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ openpyxl
7575
psycopg
7676
pymongo
7777
python-dateutil
78-
pyxform==1.9.0
78+
pyxform==2.1.1
7979
requests
8080
regex
8181
responses

dependencies/pip/requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ oauthlib==3.2.2
336336
# -r dependencies/pip/requirements.in
337337
# django-oauth-toolkit
338338
# requests-oauthlib
339-
openpyxl==3.0.9
339+
openpyxl==3.1.3
340340
# via
341341
# -r dependencies/pip/requirements.in
342342
# pyxform
@@ -412,7 +412,7 @@ pytz==2024.1
412412
# via
413413
# flower
414414
# pandas
415-
pyxform==1.9.0
415+
pyxform==2.1.1
416416
# via
417417
# -r dependencies/pip/requirements.in
418418
# formpack

jsapp/js/account/organizations/requireOrgOwner.component.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, {Suspense, useEffect} from 'react';
2+
import {useNavigate} from 'react-router-dom';
3+
import LoadingSpinner from 'js/components/common/loadingSpinner';
4+
import {ACCOUNT_ROUTES} from 'js/account/routes.constants';
5+
import {useOrganizationQuery} from 'js/account/stripe.api';
6+
import {OrganizationUserRole} from '../stripe.types';
7+
8+
interface Props {
9+
children: React.ReactNode;
10+
validRoles?: OrganizationUserRole[];
11+
mmoOnly?: boolean;
12+
redirect?: boolean;
13+
}
14+
15+
/**
16+
* Use to handle display of pages that should only be accessible to certain user roles
17+
* or members of MMOs. Defaults to allowing access for all users, so you must supply
18+
* any restrictions.
19+
*/
20+
export const ValidateOrgPermissions = ({
21+
children,
22+
validRoles = undefined,
23+
mmoOnly = false,
24+
redirect = true,
25+
}: Props) => {
26+
const navigate = useNavigate();
27+
const orgQuery = useOrganizationQuery();
28+
const hasValidRole = validRoles ? validRoles.includes(
29+
orgQuery.data?.request_user_role ?? OrganizationUserRole.member
30+
) : true;
31+
const hasValidOrg = mmoOnly ? orgQuery.data?.is_mmo : true;
32+
33+
// Redirect to Account Settings if conditions not met
34+
useEffect(() => {
35+
if (
36+
redirect &&
37+
orgQuery.data &&
38+
(!hasValidRole || !hasValidOrg)
39+
) {
40+
navigate(ACCOUNT_ROUTES.ACCOUNT_SETTINGS);
41+
}
42+
}, [redirect, orgQuery.data, navigate]);
43+
44+
return redirect && hasValidRole && hasValidOrg ? (
45+
<Suspense fallback={null}>{children}</Suspense>
46+
) : (
47+
<LoadingSpinner />
48+
);
49+
};

jsapp/js/account/routes.tsx

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from 'react';
22
import {Navigate, Route} from 'react-router-dom';
33
import RequireAuth from 'js/router/requireAuth';
4-
import {RequireOrgOwner} from 'js/account/organizations/requireOrgOwner.component';
4+
import {ValidateOrgPermissions} from 'js/account/organizations/validateOrgPermissions.component';
5+
import {OrganizationUserRole} from './stripe.types';
56
import {
67
ACCOUNT_ROUTES,
78
AccountSettings,
@@ -35,9 +36,9 @@ export default function routes() {
3536
index
3637
element={
3738
<RequireAuth>
38-
<RequireOrgOwner>
39+
<ValidateOrgPermissions validRoles={[OrganizationUserRole.owner]}>
3940
<PlansRoute />
40-
</RequireOrgOwner>
41+
</ValidateOrgPermissions>
4142
</RequireAuth>
4243
}
4344
/>
@@ -46,9 +47,9 @@ export default function routes() {
4647
index
4748
element={
4849
<RequireAuth>
49-
<RequireOrgOwner>
50+
<ValidateOrgPermissions validRoles={[OrganizationUserRole.owner]}>
5051
<AddOnsRoute />
51-
</RequireOrgOwner>
52+
</ValidateOrgPermissions>
5253
</RequireAuth>
5354
}
5455
/>
@@ -57,17 +58,31 @@ export default function routes() {
5758
index
5859
element={
5960
<RequireAuth>
60-
<RequireOrgOwner>
61+
<ValidateOrgPermissions
62+
validRoles={[
63+
OrganizationUserRole.owner,
64+
OrganizationUserRole.admin,
65+
]}
66+
>
6167
<DataStorage activeRoute={ACCOUNT_ROUTES.USAGE} />
62-
</RequireOrgOwner>
68+
</ValidateOrgPermissions>
6369
</RequireAuth>
6470
}
6571
/>
6672
<Route
6773
path={ACCOUNT_ROUTES.USAGE_PROJECT_BREAKDOWN}
6874
element={
6975
<RequireAuth>
70-
<DataStorage activeRoute={ACCOUNT_ROUTES.USAGE_PROJECT_BREAKDOWN} />
76+
<ValidateOrgPermissions
77+
validRoles={[
78+
OrganizationUserRole.owner,
79+
OrganizationUserRole.admin,
80+
]}
81+
>
82+
<DataStorage
83+
activeRoute={ACCOUNT_ROUTES.USAGE_PROJECT_BREAKDOWN}
84+
/>
85+
</ValidateOrgPermissions>
7186
</RequireAuth>
7287
}
7388
/>
@@ -93,15 +108,25 @@ export default function routes() {
93108
path={ACCOUNT_ROUTES.ORGANIZATION_MEMBERS}
94109
element={
95110
<RequireAuth>
96-
<div>Organization members view to be implemented</div>
111+
<ValidateOrgPermissions mmoOnly>
112+
<div>Organization members view to be implemented</div>
113+
</ValidateOrgPermissions>
97114
</RequireAuth>
98115
}
99116
/>
100117
<Route
101118
path={ACCOUNT_ROUTES.ORGANIZATION_SETTINGS}
102119
element={
103120
<RequireAuth>
104-
<div>Organization settings view to be implemented</div>
121+
<ValidateOrgPermissions
122+
validRoles={[
123+
OrganizationUserRole.owner,
124+
OrganizationUserRole.admin,
125+
]}
126+
mmoOnly
127+
>
128+
<div>Organization settings view to be implemented</div>
129+
</ValidateOrgPermissions>
105130
</RequireAuth>
106131
}
107132
/>

jsapp/js/envStore.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ export interface EnvironmentResponse {
2222
transcription_languages: TransxLanguages;
2323
translation_languages: TransxLanguages;
2424
submission_placeholder: string;
25-
// TODO: Remove optional marker when PR#5182 is merged
26-
use_team_label?: boolean;
25+
use_team_label: boolean;
2726
frontend_min_retry_time: number;
2827
frontend_max_retry_time: number;
2928
asr_mt_features_enabled: boolean;
@@ -215,8 +214,7 @@ class EnvStore {
215214
this.data.project_metadata_fields = response.project_metadata_fields;
216215
this.data.user_metadata_fields = response.user_metadata_fields;
217216
this.data.submission_placeholder = response.submission_placeholder;
218-
// TODO: Assign response value when PR#5182 is merged
219-
this.data.use_team_label = true;
217+
this.data.use_team_label = response.use_team_label;
220218
this.data.mfa_localized_help_text = response.mfa_localized_help_text;
221219
this.data.mfa_enabled = response.mfa_enabled;
222220
this.data.mfa_per_user_availability = response.mfa_per_user_availability;

kobo/apps/audit_log/audit_actions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22

33

44
class AuditAction(models.TextChoices):
5+
ADD_MEDIA = 'add-media'
56
ARCHIVE = 'archive'
67
AUTH = 'auth'
8+
CONNECT_PROJECT = 'connect-project'
79
CREATE = 'create'
810
DELETE = 'delete'
11+
DELETE_MEDIA = 'delete-media'
12+
DELETE_SERVICE = 'delete-service'
913
DEPLOY = 'deploy'
1014
DISABLE_SHARING = 'disable-sharing'
15+
DISCONNECT_PROJECT = 'disconnect-project'
1116
ENABLE_SHARING = 'enable-sharing'
1217
IN_TRASH = 'in-trash'
18+
MODIFY_IMPORTED_FIELDS = 'modify-imported-fields'
19+
MODIFY_SERVICE = 'modify-service'
1320
MODIFY_SHARING = 'modify_sharing'
1421
PUT_BACK = 'put-back'
1522
REDEPLOY = 'redeploy'
23+
REGISTER_SERVICE = 'register-service'
1624
REMOVE = 'remove'
1725
UNARCHIVE = 'unarchive'
1826
UPDATE = 'update'

kobo/apps/audit_log/base_views.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,32 +49,40 @@ def get_object(self):
4949
return obj
5050
audit_log_data = {}
5151
for field in self.logged_fields:
52-
value = get_nested_field(obj, field)
53-
audit_log_data[field] = value
52+
field_path = field[1] if isinstance(field, tuple) else field
53+
field_label = field[0] if isinstance(field, tuple) else field
54+
value = get_nested_field(obj, field_path)
55+
audit_log_data[field_label] = value
5456
self.request._request.initial_data = audit_log_data
5557
return obj
5658

5759
def perform_update(self, serializer):
5860
self.perform_update_override(serializer)
5961
audit_log_data = {}
6062
for field in self.logged_fields:
61-
value = get_nested_field(serializer.instance, field)
62-
audit_log_data[field] = value
63+
field_path = field[1] if isinstance(field, tuple) else field
64+
field_label = field[0] if isinstance(field, tuple) else field
65+
value = get_nested_field(serializer.instance, field_path)
66+
audit_log_data[field_label] = value
6367
self.request._request.updated_data = audit_log_data
6468

6569
def perform_create(self, serializer):
6670
self.perform_create_override(serializer)
6771
audit_log_data = {}
6872
for field in self.logged_fields:
69-
value = get_nested_field(serializer.instance, field)
70-
audit_log_data[field] = value
73+
field_path = field[1] if isinstance(field, tuple) else field
74+
field_label = field[0] if isinstance(field, tuple) else field
75+
value = get_nested_field(serializer.instance, field_path)
76+
audit_log_data[field_label] = value
7177
self.request._request.updated_data = audit_log_data
7278

7379
def perform_destroy(self, instance):
7480
audit_log_data = {}
7581
for field in self.logged_fields:
76-
value = get_nested_field(instance, field)
77-
audit_log_data[field] = value
82+
field_path = field[1] if isinstance(field, tuple) else field
83+
field_label = field[0] if isinstance(field, tuple) else field
84+
value = get_nested_field(instance, field_path)
85+
audit_log_data[field_label] = value
7886
self.request._request.initial_data = audit_log_data
7987
self.perform_destroy_override(instance)
8088

0 commit comments

Comments
 (0)