Skip to content

Commit c141cf3

Browse files
authored
Merge pull request #154 from open-data/feature/ds-reference-tables
Reference Tables & Org Specific Fields
2 parents d99e6c9 + a244bdd commit c141cf3

15 files changed

Lines changed: 264 additions & 74 deletions

File tree

changes/154.a.changes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added output functionality to the Data Dictionary, Excel References, and JSON Schemas to display Organizaion based choices.

changes/154.a.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a new subcommand `create-ref-tables` which executes pSQL scripts defined in the `recombinant.reference_definitions` config option. This allows for creation of advanced DataStore tables outside of the CKAN framework. These tables are not editable by CKAN users.

changes/154.b.changes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved the `format_trigger_error` to support multiple splitting of the private code point `\uF8FF`. This allows for more dynamic data error messages, while still having the text go through the translation interfaces properly.

changes/154.b.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Recombinant schemas now support `choices_reference_table` definition for fields, which can reference an existing table in the DataStore database. Choices are filterable with `choices_filter_query` which accepts a basic pSQL WHERE clause.

changes/154.c.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Recombinant schemas now support `choices_fiscal_year` definition for fields. This is a YAML Mapping which can take `min_year (default: 2005)`, `max_year (default: current)`, and `month_start (default: 4/April)`
Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,29 @@
11
window.addEventListener('load', function(){
22
$(document).ready(function() {
3-
43
// Code example switching
54
let codeExampleControl = $('#api-access-example-code-control');
65
let codeBlocks = $('#api-access-accordion').find('figure');
7-
86
if( codeExampleControl.length > 0 ){
9-
107
let controlButtons = $(codeExampleControl).find('input[name="api-access-example-code"]');
11-
128
if( controlButtons.length > 0 ){
13-
149
$(controlButtons).on('change', function(_event){
15-
1610
let selectedCode = $(codeExampleControl).find('input[name="api-access-example-code"]:checked').val();
17-
1811
$(codeBlocks).each(function(_index, _codeBlock){
19-
2012
if( $(_codeBlock).hasClass(selectedCode) ){
21-
2213
$(_codeBlock).show();
23-
2414
}else{
25-
2615
$(_codeBlock).hide();
27-
2816
}
29-
3017
});
31-
3218
});
33-
3419
}
35-
3620
}
3721

3822
// Activity tab link
3923
let activityTab = $('#activity-lnk');
40-
4124
if( activityTab.length > 0 ){
42-
4325
let link = $('#activity').find('a').first().attr('href');
44-
4526
if( link && link.length > 0 ){
46-
4727
$(activityTab).attr('href', link);
4828
$(activityTab).removeAttr('aria-controls');
4929
$(activityTab).attr('tabindex', 0);
@@ -59,6 +39,7 @@ window.addEventListener('load', function(){
5939
_event.preventDefault();
6040
_goto_activity();
6141
});
42+
6243
$(activityTab).on('keyup.Link', function(_event){
6344
let keyCode = _event.keyCode ? _event.keyCode : _event.which;
6445
// space and enter keys required for a11y
@@ -67,10 +48,15 @@ window.addEventListener('load', function(){
6748
_goto_activity();
6849
}
6950
});
70-
7151
}
72-
7352
}
7453

54+
// Rebind fix for wet-boew tab redraw
55+
let dictionaryTables = $('table.recombinant-data-dictionary[data-module="table-toggle-more"]');
56+
if( dictionaryTables && dictionaryTables.length > 0 && typeof ckan != 'undefined' ){
57+
$(dictionaryTables).each(function(_index, _table){
58+
ckan.module.initialize(_table);
59+
});
60+
}
7561
});
7662
});

ckanext/recombinant/cli.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@
55
import json
66
import re
77
from openpyxl.formula import Tokenizer
8+
import sqlalchemy as sa
89

910
from typing import Dict, List, Any, Optional, TextIO
1011

1112
from ckan.logic import ValidationError
1213
from ckanapi import LocalCKAN, NotFound
14+
from ckanext.datastore.backend import DatastoreBackend
15+
from ckanext.datastore.backend.postgres import DatastorePostgresqlBackend
1316

1417
from ckanext.recombinant.tables import (
1518
get_dataset_type_for_resource_name,
1619
get_dataset_types,
1720
get_chromo,
1821
get_geno,
1922
get_target_datasets,
20-
get_resource_names
23+
get_resource_names,
24+
get_reference_tables_sql,
2125
)
2226
from ckanext.recombinant.read_csv import csv_data_batch
2327
from ckanext.recombinant.write_excel import excel_template
@@ -910,3 +914,17 @@ def _template(dataset_type: str,
910914
with open(output_file, 'w') as out:
911915
# type_ignore_reason: incomplete typing
912916
tmpl.save(out) # type: ignore
917+
918+
919+
@recombinant.command(short_help="Run all the sql scripts "
920+
"from recombinant.reference_definitions")
921+
def create_ref_tables():
922+
"""
923+
Run all the sql scripts from recombinant.reference_definitions
924+
"""
925+
# type_ignore_reason: incomplete typing
926+
backend: DatastorePostgresqlBackend = DatastoreBackend.\
927+
get_active_backend() # type: ignore
928+
with backend._get_write_engine().begin() as connection:
929+
connection.execute(sa.text(get_reference_tables_sql()))
930+
click.echo('Done!')

ckanext/recombinant/errors.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,5 @@ def format_trigger_error(error_values: List[str]) -> Generator[str, None, None]:
2929
replacements, allowing i18n support in the framework.
3030
"""
3131
for e in error_values:
32-
if '\uF8FF' in e:
33-
yield _(e.split('\uF8FF')[0]).format(e.split('\uF8FF')[1])
34-
else:
35-
yield _(e)
32+
err, *args = e.split('\uF8FF')
33+
yield _(err).format(*args)

ckanext/recombinant/helpers.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
import os.path
33
from markupsafe import Markup
4+
import sqlalchemy as sa
5+
from datetime import datetime
46

57
from typing import Dict, Any, Optional, List, Union
68

@@ -9,6 +11,13 @@
911
import ckanapi
1012
from ckan.lib.helpers import lang
1113

14+
from ckanext.datastore.backend import DatastoreBackend
15+
from ckanext.datastore.backend.postgres import (
16+
DatastorePostgresqlBackend,
17+
identifier,
18+
literal_string
19+
)
20+
1221
from ckanext.recombinant.tables import (
1322
get_chromo, get_geno, get_dataset_types,
1423
get_published_resource_resource_name
@@ -129,9 +138,24 @@ def recombinant_example(resource_name: str,
129138
return left[2:] + ('\n' + left[2:]).join(out.split('\n')[1:-1])
130139

131140

132-
def recombinant_choice_fields(resource_name: str,
133-
all_languages: bool = False,
134-
prefer_lang: Optional[str] = None) -> Dict[str, Any]:
141+
def get_choices_fiscal_year(min_year: int = 2005,
142+
max_year: Optional[int] = None,
143+
month_start: int = 4) -> List[str]:
144+
"""
145+
Dynamically generate choices for fiscal years.
146+
"""
147+
if not max_year:
148+
max_year = datetime.now().year if \
149+
datetime.now().month >= month_start else datetime.now().year - 1
150+
return [f'{i}-{i + 1}'
151+
for i in range(min_year, max_year + 1)]
152+
153+
154+
def recombinant_choice_fields(
155+
resource_name: str,
156+
all_languages: bool = False,
157+
prefer_lang: Optional[str] = None,
158+
org_name: Optional[str] = None) -> Dict[str, Any]:
135159
"""
136160
Return a datastore_id: choices dict from the resource definition
137161
that contain lists of choices, with labels pre-translated
@@ -168,11 +192,42 @@ def key_fn(v: str) -> Any:
168192
if v not in exclude_choices
169193
]
170194

195+
def build_choices_psql(f: Dict[str, Any]):
196+
# type_ignore_reason: incomplete typing
197+
backend: DatastorePostgresqlBackend = DatastoreBackend.\
198+
get_active_backend() # type: ignore
199+
with backend._get_read_engine().begin() as connection:
200+
filter_clause = f.get('choices_filter_query', '')
201+
if filter_clause and "{org}" in filter_clause and not org_name:
202+
filter_clause = '' # if no org_name passed, cannot query it
203+
filter_clause = filter_clause.format(
204+
org=literal_string(org_name) if org_name else '')
205+
results = connection.execute(sa.text("""
206+
SELECT * FROM {ref_table} {filter_clause}
207+
ORDER BY {ds_id} ASC;
208+
""".format(
209+
ref_table=identifier(f['choices_reference_table']),
210+
filter_clause=filter_clause,
211+
ds_id=identifier(f['datastore_id'])
212+
).replace(':', r'\:'))).mappings().fetchall() # avoid bind params
213+
out[f['datastore_id']] = [
214+
(r[f['datastore_id']],
215+
dict(en=r['label_en'],
216+
fr=r['label_fr'],
217+
valid_orgs=r.get('org_years') or {})
218+
if all_languages else
219+
r['label_%s' % (prefer_lang or lang())]) for r in results]
220+
171221
for f in chromo['fields']:
172222
if 'choices' in f:
173223
build_choices(f, f['choices'])
174224
elif 'choices_file' in f and '_path' in chromo:
175225
build_choices(f, _read_choices_file(chromo, f))
226+
elif 'choices_reference_table' in f:
227+
build_choices_psql(f)
228+
elif 'choices_fiscal_year' in f:
229+
out[f['datastore_id']] = [
230+
(v, v) for v in get_choices_fiscal_year(**f['choices_fiscal_year'])]
176231

177232
return out
178233

ckanext/recombinant/tables.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
55
This module provides access to those definitions.
66
"""
7+
import importlib
8+
import os
9+
710
from typing import List, Dict, Optional, Any
811

912
import ckan.plugins as p
1013

14+
from ckanext.datastore.cli import _parse_db_config
1115
from ckanext.recombinant.errors import RecombinantException
1216

1317

@@ -97,3 +101,27 @@ def get_target_datasets() -> List[str]:
97101
"""
98102
genos = _get_plugin()._genos
99103
return sorted((t['target_dataset'] for t in genos.values()))
104+
105+
106+
def get_reference_tables_sql():
107+
"""
108+
Compiles all sql scripts from recombinant.reference_definitions
109+
"""
110+
ref_tables_uris = p.toolkit.config.get('recombinant.reference_definitions',
111+
"").split()
112+
write_url = _parse_db_config('ckan.datastore.write_url')
113+
read_url = _parse_db_config('ckan.datastore.read_url')
114+
sql = ""
115+
for uri in ref_tables_uris:
116+
module, file_name = uri.split(':', 1)
117+
try:
118+
m = importlib.import_module(module)
119+
except ImportError:
120+
raise RecombinantException('Could not load module path %s' % uri)
121+
_p = m.__path__[0]
122+
_p = os.path.join(_p, file_name)
123+
if not os.path.exists(_p):
124+
raise RecombinantException('File path does not exist %s' % uri)
125+
with open(_p) as f:
126+
sql += f.read()
127+
return sql.format(readuser=read_url['db_user'], writeuser=write_url['db_user'])

0 commit comments

Comments
 (0)