Skip to content

Commit f54f77d

Browse files
authored
Merge pull request #102 from guzman-raphael/uuid_delete
Fix issue with uuid restricitons
2 parents c43a7f6 + 4f5c6ec commit f54f77d

File tree

5 files changed

+78
-20
lines changed

5 files changed

+78
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Observes [Semantic Versioning](https://semver.org/spec/v2.0.0.html) standard and
1818
### Removed
1919
- `InvalidDeleteRequest` exception is no longer available as it is now allowed to delete more than 1 record at a time. PR #99
2020

21+
### Fixed
22+
- `uuid` types not properly restricted on `GET /record`, `DELETE /record`, and `GET /dependency`. PR #102
23+
2124
## [0.1.0b2] - 2021-03-12
2225

2326
### Fixed

pharus/interface.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,12 @@ def _fetch_records(jwt_payload: dict, schema_name: str, table_name: str,
131131

132132
# Get table object from name
133133
table = _DJConnector._get_table_object(schema_virtual_module, table_name)
134-
134+
attributes = table.heading.attributes
135135
# Fetch tuples without blobs as dict to be used to create a
136136
# list of tuples for returning
137-
query = table & dj.AndList([_DJConnector._filter_to_restriction(f)
138-
for f in restriction])
137+
query = table & dj.AndList([
138+
_DJConnector._filter_to_restriction(f, attributes[f['attributeName']].type)
139+
for f in restriction])
139140
non_blobs_rows = query.fetch(*table.heading.non_blobs, as_dict=True, limit=limit,
140141
offset=(page-1)*limit, order_by=order)
141142

@@ -149,7 +150,7 @@ def _fetch_records(jwt_payload: dict, schema_name: str, table_name: str,
149150
row = []
150151
# Loop through each attributes, append to the tuple_to_return with specific
151152
# modification based on data type
152-
for attribute_name, attribute_info in table.heading.attributes.items():
153+
for attribute_name, attribute_info in attributes.items():
153154
if not attribute_info.is_blob:
154155
if non_blobs_row[attribute_name] is None:
155156
# If it is none then just append None
@@ -180,7 +181,7 @@ def _fetch_records(jwt_payload: dict, schema_name: str, table_name: str,
180181

181182
# Add the row list to tuples
182183
rows.append(row)
183-
return list(table.heading.attributes.keys()), rows, len(query)
184+
return list(attributes.keys()), rows, len(query)
184185

185186
@staticmethod
186187
def _get_table_attributes(jwt_payload: dict, schema_name: str, table_name: str) -> dict:
@@ -295,12 +296,14 @@ def _record_dependency(jwt_payload: dict, schema_name: str, table_name: str,
295296
_DJConnector._set_datajoint_config(jwt_payload)
296297
virtual_module = dj.VirtualModule(schema_name, schema_name)
297298
table = getattr(virtual_module, table_name)
299+
attributes = table.heading.attributes
298300
# Retrieve dependencies of related to retricted
299301
dependencies = [dict(schema=descendant.database, table=descendant.table_name,
300302
accessible=True, count=len(
301303
(table if descendant.full_table_name == table.full_table_name
302304
else descendant * table) & dj.AndList([
303-
_DJConnector._filter_to_restriction(f)
305+
_DJConnector._filter_to_restriction(
306+
f, attributes[f['attributeName']].type)
304307
for f in restriction])))
305308
for descendant in table().descendants(as_objects=True)]
306309
return dependencies
@@ -352,8 +355,10 @@ def _delete_records(jwt_payload: dict, schema_name: str, table_name: str,
352355

353356
# Get table object from name
354357
table = _DJConnector._get_table_object(schema_virtual_module, table_name)
355-
356-
restrictions = [_DJConnector._filter_to_restriction(f) for f in restriction]
358+
attributes = table.heading.attributes
359+
restrictions = [
360+
_DJConnector._filter_to_restriction(f, attributes[f['attributeName']].type)
361+
for f in restriction]
357362

358363
# Compute restriction
359364
query = table & dj.AndList(restrictions)
@@ -385,13 +390,15 @@ def _get_table_object(schema_virtual_module: VirtualModule, table_name: str) ->
385390
return getattr(schema_virtual_module, table_name_parts[0])
386391

387392
@staticmethod
388-
def _filter_to_restriction(attribute_filter: dict) -> str:
393+
def _filter_to_restriction(attribute_filter: dict, attribute_type: str) -> str:
389394
"""
390395
Convert attribute filter to a restriction.
391396
392397
:param attribute_filter: A filter as ``dict`` with ``attributeName``, ``operation``,
393398
``value`` keys defined, defaults to ``[]``
394399
:type attribute_filter: dict
400+
:param attribute_type: Attribute type
401+
:type attribute_type: str
395402
:return: DataJoint-compatible restriction
396403
:rtype: str
397404
"""
@@ -405,7 +412,8 @@ def _filter_to_restriction(attribute_filter: dict) -> str:
405412

406413
if (isinstance(attribute_filter['value'], str) and
407414
not attribute_filter['value'].isnumeric()):
408-
value = f"'{attribute_filter['value']}'"
415+
value = (f"X'{attribute_filter['value'].replace('-', '')}'"
416+
if attribute_type == 'uuid' else f"'{attribute_filter['value']}'")
409417
else:
410418
value = ('NULL' if attribute_filter['value'] is None
411419
else attribute_filter['value'])

tests/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
from pharus.server import app
3+
from uuid import UUID
34
from os import getenv
45
import datajoint as dj
56
from datetime import date
@@ -136,6 +137,22 @@ class Student(dj.Lookup):
136137
Student.drop()
137138

138139

140+
@pytest.fixture
141+
def Computer(schema_main):
142+
"""Computer table for testing."""
143+
@schema_main
144+
class Computer(dj.Lookup):
145+
definition = """
146+
computer_id: uuid
147+
---
148+
computer_brand: enum('HP', 'DELL')
149+
"""
150+
contents = [(UUID('ffffffff-86d5-4af7-a013-89bde75528bd'), 'HP'),
151+
(UUID('aaaaaaaa-86d5-4af7-a013-89bde75528bd'), 'DELL')]
152+
yield Computer
153+
Computer.drop()
154+
155+
139156
@pytest.fixture
140157
def Int(schema_main):
141158
"""Integer basic table for testing."""

tests/test_delete.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from . import SCHEMA_PREFIX, token, client, connection, schemas_simple
1+
from . import SCHEMA_PREFIX, token, client, connection, schemas_simple, schema_main, Computer
22
import datajoint as dj
33
from json import dumps
44
from base64 import b64encode
55
from urllib.parse import urlencode
6+
from uuid import UUID
67

78

89
def test_delete_dependent_with_cascade(token, client, connection, schemas_simple):
@@ -72,3 +73,18 @@ def test_delete_invalid(token, client, connection, schemas_simple):
7273
assert REST_response.status_code == 500
7374
assert b'Nothing to delete' in REST_response.data
7475
assert len(getattr(vm, table_name)()) == 3
76+
77+
78+
def test_delete_uuid_primary(token, client, Computer):
79+
"""Verify can delete if restricting by UUID."""
80+
uuid_val = 'aaaaaaaa-86d5-4af7-a013-89bde75528bd'
81+
restriction = [dict(attributeName='computer_id', operation='=',
82+
value=uuid_val)]
83+
encoded_restriction = b64encode(dumps(restriction).encode('utf-8')).decode('utf-8')
84+
q = dict(limit=10, page=1, order='computer_id DESC',
85+
restriction=encoded_restriction)
86+
REST_response = client.delete(
87+
f'/schema/{Computer.database}/table/{"Computer"}/record?{urlencode(q)}',
88+
headers=dict(Authorization=f'Bearer {token}'))
89+
assert REST_response.status_code == 200
90+
assert len(Computer() & dict(computer_id=UUID(uuid_val))) == 0

tests/test_filter.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from base64 import b64encode
33
from urllib.parse import urlencode
44
from datetime import date, datetime
5-
from . import token, client, connection, schema_main, Student
5+
from . import token, client, connection, schema_main, Student, Computer
66

77

88
def test_filters(token, client, Student):
@@ -15,8 +15,8 @@ def test_filters(token, client, Student):
1515
q = dict(limit=10, page=1, order='student_enroll_date DESC',
1616
restriction=encoded_restriction)
1717
REST_records = client.get(
18-
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
19-
headers=dict(Authorization=f'Bearer {token}')).json['records']
18+
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
19+
headers=dict(Authorization=f'Bearer {token}')).json['records']
2020
assert len(REST_records) == 10
2121
assert REST_records[0][3] == datetime(2021, 1, 16).timestamp()
2222
# 'equal' null
@@ -25,8 +25,8 @@ def test_filters(token, client, Student):
2525
q = dict(limit=10, page=2, order='student_id ASC',
2626
restriction=encoded_restriction)
2727
REST_records = client.get(
28-
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
29-
headers=dict(Authorization=f'Bearer {token}')).json['records']
28+
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
29+
headers=dict(Authorization=f'Bearer {token}')).json['records']
3030
assert len(REST_records) == 10
3131
assert all([r[5] is None for r in REST_records])
3232
assert REST_records[0][0] == 34
@@ -36,8 +36,8 @@ def test_filters(token, client, Student):
3636
q = dict(limit=10, page=1, order='student_id ASC',
3737
restriction=encoded_restriction)
3838
REST_records = client.get(
39-
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
40-
headers=dict(Authorization=f'Bearer {token}')).json['records']
39+
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
40+
headers=dict(Authorization=f'Bearer {token}')).json['records']
4141
assert len(REST_records) == 10
4242
assert all([r[0] != 2 for r in REST_records])
4343
assert REST_records[-1][0] == 10
@@ -48,8 +48,22 @@ def test_filters(token, client, Student):
4848
q = dict(limit=10, page=1, order='student_id ASC',
4949
restriction=encoded_restriction)
5050
REST_records = client.get(
51-
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
52-
headers=dict(Authorization=f'Bearer {token}')).json['records']
51+
f'/schema/{Student.database}/table/{"Student"}/record?{urlencode(q)}',
52+
headers=dict(Authorization=f'Bearer {token}')).json['records']
5353
assert len(REST_records) == 1
5454
assert REST_records[0][1] == 'Norma Fisher'
5555
assert REST_records[0][6] == 0
56+
57+
58+
def test_uuid_filter(token, client, Computer):
59+
"""Verify UUID can be properly restricted."""
60+
restriction = [dict(attributeName='computer_id', operation='=',
61+
value='aaaaaaaa-86d5-4af7-a013-89bde75528bd')]
62+
encoded_restriction = b64encode(dumps(restriction).encode('utf-8')).decode('utf-8')
63+
q = dict(limit=10, page=1, order='computer_id DESC',
64+
restriction=encoded_restriction)
65+
REST_records = client.get(
66+
f'/schema/{Computer.database}/table/{"Computer"}/record?{urlencode(q)}',
67+
headers=dict(Authorization=f'Bearer {token}')).json['records']
68+
assert len(REST_records) == 1
69+
assert REST_records[0][1] == 'DELL'

0 commit comments

Comments
 (0)