Skip to content

Commit 76a82a1

Browse files
Merge branch 'beta' of github.com:kobotoolbox/kpi into beta
2 parents 79bcb4a + 16506e8 commit 76a82a1

File tree

11 files changed

+161
-14
lines changed

11 files changed

+161
-14
lines changed

dependencies/pip/dev_requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Fabric
44
coverage
55
coveralls
6+
ddt
67
ipython==8.12.3 # Max supported version by Python 3.8
78
mock
89
model-bakery

dependencies/pip/dev_requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ cryptography==42.0.5
114114
# pyopenssl
115115
cssselect==1.2.0
116116
# via pyquery
117+
ddt==1.7.2
118+
# via -r dependencies/pip/dev_requirements.in
117119
decorator==5.1.1
118120
# via
119121
# fabric
@@ -598,6 +600,8 @@ tzdata==2024.1
598600
# celery
599601
# django-celery-beat
600602
# pandas
603+
ua-parser==0.18.0
604+
# via -r dependencies/pip/requirements.in
601605
uritemplate==4.1.1
602606
# via google-api-python-client
603607
urllib3==1.26.18

dependencies/pip/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ shortuuid
8484
sqlparse
8585
static3
8686
tabulate
87+
ua-parser
8788
uWSGI
8889
Werkzeug
8990
xlrd

dependencies/pip/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,8 @@ tzdata==2024.1
495495
# celery
496496
# django-celery-beat
497497
# pandas
498+
ua-parser==0.18.0
499+
# via -r dependencies/pip/requirements.in
498500
uritemplate==4.1.1
499501
# via google-api-python-client
500502
urllib3==1.26.18

jsapp/js/account/security/email/emailSection.component.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Button from 'jsapp/js/components/common/button';
1111
import TextBox from 'jsapp/js/components/common/textBox';
1212
import Icon from 'jsapp/js/components/common/icon';
1313
import {formatTime} from 'jsapp/js/utils';
14+
import {notify} from 'js/utils';
1415

1516
interface EmailState {
1617
emails: EmailResponse[];
@@ -87,6 +88,15 @@ export default function EmailSection() {
8788
});
8889
}
8990

91+
function handleSubmit() {
92+
const emailPattern = /[^@]+@[^@]+\.[^@]+/;
93+
if (!emailPattern.test(email.newEmail)) {
94+
notify.error('Invalid email address');
95+
} else {
96+
setNewUserEmail(email.newEmail);
97+
}
98+
}
99+
90100
const currentAccount = session.currentAccount;
91101
const unverifiedEmail = email.emails.find(
92102
(userEmail) => !userEmail.verified && !userEmail.primary
@@ -162,7 +172,7 @@ export default function EmailSection() {
162172
className={style.optionsSection}
163173
onSubmit={(e) => {
164174
e.preventDefault();
165-
setNewUserEmail(email.newEmail);
175+
handleSubmit();
166176
}}
167177
>
168178
{/*TODO: Move TextBox into a modal--it messes up the flow of the row right now*/}
@@ -178,10 +188,7 @@ export default function EmailSection() {
178188
size='m'
179189
color='blue'
180190
type='frame'
181-
onClick={setNewUserEmail.bind(setNewUserEmail, email.newEmail)}
182-
// quick simple subtle email validation to avoid complete accidents
183-
// a toast showing any API error feedback would be nicer
184-
isDisabled={!/[^@]+@[^@]+\.[^@]+/.test(email.newEmail)}
191+
onClick={handleSubmit}
185192
/>
186193
</form>
187194
</div>

jsapp/js/i18nMissingStrings.es6

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
// misc strings
99
test = t('Add another condition');
1010

11-
test = t('Question should match all of these criteria');
12-
test = t('Question should match any of these criteria');
11+
test = t('Previous responses should match all of these conditions');
12+
test = t('Previous responses should match any of these conditions');
1313
test = t('Click to add another response...');
1414
test = t('AUTOMATIC');
1515
test = t('Skip Logic');

jsapp/xlform/src/view.rowDetail.SkipLogic.coffee

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ module.exports = do ->
2929
<button class="skiplogic__addcriterion kobo-button kobo-button--storm">+ #{t("Add another condition")}</button>
3030
</p>
3131
<select class="skiplogic__delimselect">
32-
<option value="and">#{t("Question should match all of these criteria")}</option>
33-
<option value="or">#{t("Question should match any of these criteria")}</option>
32+
<option value="and">#{t("Previous responses should match all of these conditions")}</option>
33+
<option value="or">#{t("Previous responses should match any of these conditions")}</option>
3434
</select>
3535
""")
3636

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Generated by Django 4.2.11 on 2024-07-31 20:00
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
6+
7+
def manually_create_indexes_instructions(apps, schema_editor):
8+
print(
9+
"""
10+
!!! ATTENTION !!!
11+
If you have existing projects you need to run the SQL queries below in PostgreSQL directly:
12+
13+
> CREATE INDEX CONCURRENTLY audit_log_asset_action_idx ON public.audit_log_auditlog
14+
USING btree (((metadata -> 'asset_uid'::text)), action);
15+
> CREATE INDEX CONCURRENTLY audit_log_asset_uid_idx ON public.audit_log_auditlog
16+
USING btree (((metadata -> 'asset_uid'::text)))
17+
18+
Otherwise, the audit log table may be unusable.
19+
"""
20+
)
21+
22+
23+
def manually_drop_indexes_instructions(apps, schema_editor):
24+
print(
25+
"""
26+
!!! ATTENTION !!!
27+
Run the SQL queries below in PostgreSQL directly:
28+
29+
> DROP INDEX IF EXISTS "audit_log_asset_action_idx";
30+
> DROP INDEX IF EXISTS "audit_log_asset_uid_idx";
31+
32+
"""
33+
)
34+
35+
36+
class Migration(migrations.Migration):
37+
38+
dependencies = [
39+
('audit_log', '0001_squashed_0006_remove_index_together'),
40+
]
41+
42+
operations = [
43+
migrations.AlterField(
44+
model_name='auditlog',
45+
name='action',
46+
field=models.CharField(choices=[('create', 'CREATE'), ('delete', 'DELETE'), ('in-trash', 'IN TRASH'), ('put-back', 'PUT BACK'), ('remove', 'REMOVE'), ('update', 'UPDATE'), ('auth', 'AUTH')], db_index=True, default='delete', max_length=10),
47+
),
48+
]
49+
if not settings.SKIP_HEAVY_MIGRATIONS:
50+
operations.extend(
51+
[
52+
migrations.AddIndex(
53+
model_name='auditlog',
54+
index=models.Index(models.F('metadata__asset_uid'), models.F('action'),
55+
name='audit_log_asset_action_idx'),
56+
),
57+
migrations.AddIndex(
58+
model_name='auditlog',
59+
index=models.Index(models.F('metadata__asset_uid'), name='audit_log_asset_uid_idx'),
60+
),
61+
]
62+
)
63+
else:
64+
operations.extend([
65+
migrations.RunPython(
66+
manually_create_indexes_instructions,
67+
manually_drop_indexes_instructions,
68+
)
69+
])

kobo/apps/audit_log/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class AuditAction(models.TextChoices):
1414
PUT_BACK = 'put-back', 'PUT BACK'
1515
REMOVE = 'remove', 'REMOVE'
1616
UPDATE = 'update', 'UPDATE'
17+
AUTH = 'auth', 'AUTH'
1718

1819

1920
class AuditLog(models.Model):
@@ -39,6 +40,8 @@ class Meta:
3940
indexes = [
4041
models.Index(fields=['app_label', 'model_name', 'action']),
4142
models.Index(fields=['app_label', 'model_name']),
43+
models.Index(models.F('metadata__asset_uid'), 'action', name='audit_log_asset_action_idx'),
44+
models.Index(models.F('metadata__asset_uid'), name='audit_log_asset_uid_idx')
4245
]
4346

4447
def save(
Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,67 @@
11
# coding: utf-8
2+
from ddt import data, ddt, unpack
23
from django.test.client import RequestFactory
34

45
from kobo.apps.openrosa.apps.main.tests.test_base import TestBase
5-
from kobo.apps.openrosa.libs.utils.viewer_tools import export_def_from_filename,\
6-
get_client_ip
6+
from kobo.apps.openrosa.libs.utils.viewer_tools import (
7+
export_def_from_filename,
8+
get_client_ip,
9+
get_human_readable_client_user_agent,
10+
)
711

812

13+
@ddt
914
class TestViewerTools(TestBase):
1015
def test_export_def_from_filename(self):
11-
filename = "path/filename.xlsx"
16+
filename = 'path/filename.xlsx'
1217
ext, mime_type = export_def_from_filename(filename)
1318
self.assertEqual(ext, 'xlsx')
1419
self.assertEqual(mime_type, 'vnd.openxmlformats')
1520

1621
def test_get_client_ip(self):
17-
request = RequestFactory().get("/")
22+
request = RequestFactory().get('/')
1823
client_ip = get_client_ip(request)
1924
self.assertIsNotNone(client_ip)
2025
# will this always be 127.0.0.1
21-
self.assertEqual(client_ip, "127.0.0.1")
26+
self.assertEqual(client_ip, '127.0.0.1')
27+
28+
@data(
29+
# chrome on android phone
30+
(
31+
'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36'
32+
' (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36',
33+
'Chrome Mobile (Android)',
34+
),
35+
# chrome on Windows
36+
(
37+
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko)'
38+
' Chrome/47.0.2526.111 Safari/537.36',
39+
'Chrome (Windows)',
40+
),
41+
# firefox on linux
42+
(
43+
'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1',
44+
'Firefox (Ubuntu)',
45+
),
46+
# curl
47+
('curl/7.88.1', 'curl (Other)'),
48+
# system browser on iPhone
49+
(
50+
'Mozilla/5.0 (Apple-iPhone7C2/1202.466; U; CPU like Mac OS X; en) AppleWebKit/420+'
51+
' (KHTML, like Gecko) Version/3.0 Mobile/1A543 Safari/419.3',
52+
'Mobile Safari (iOS)',
53+
),
54+
# nonsense
55+
('Lizards!', 'Other (Other)'),
56+
# empty
57+
('', 'No information available'),
58+
# missing
59+
(None, 'No information available'),
60+
)
61+
@unpack
62+
def test_client_user_agent(self, ua_string, expected_result):
63+
factory = RequestFactory()
64+
header = {'HTTP_USER_AGENT': ua_string}
65+
request = factory.get('/', **header)
66+
result = get_human_readable_client_user_agent(request)
67+
self.assertEqual(result, expected_result)

0 commit comments

Comments
 (0)