ckanext-csvwmapandtransform 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ckanext/csvwmapandtransform/__init__.py +0 -0
- ckanext/csvwmapandtransform/action.py +405 -0
- ckanext/csvwmapandtransform/assets/script.js +81 -0
- ckanext/csvwmapandtransform/assets/style.css +124 -0
- ckanext/csvwmapandtransform/assets/webassets.yml +13 -0
- ckanext/csvwmapandtransform/auth.py +23 -0
- ckanext/csvwmapandtransform/cli.py +18 -0
- ckanext/csvwmapandtransform/db.py +397 -0
- ckanext/csvwmapandtransform/helpers.py +67 -0
- ckanext/csvwmapandtransform/mapper.py +92 -0
- ckanext/csvwmapandtransform/plugin.py +140 -0
- ckanext/csvwmapandtransform/tasks.py +257 -0
- ckanext/csvwmapandtransform/templates/csvwmapandtransform/create_mapping.html +56 -0
- ckanext/csvwmapandtransform/templates/csvwmapandtransform/transform.html +108 -0
- ckanext/csvwmapandtransform/templates/package/resource_read.html +8 -0
- ckanext/csvwmapandtransform/templates/package/snippets/resource_item.html +23 -0
- ckanext/csvwmapandtransform/tests/__init__.py +0 -0
- ckanext/csvwmapandtransform/views.py +205 -0
- ckanext_csvwmapandtransform-1.0.0-py3.13-nspkg.pth +1 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/METADATA +121 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/RECORD +26 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/WHEEL +5 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/entry_points.txt +2 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/licenses/LICENSE +661 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/namespace_packages.txt +1 -0
- ckanext_csvwmapandtransform-1.0.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import ckan.plugins.toolkit as toolkit
|
|
2
|
+
|
|
3
|
+
if toolkit.check_ckan_version("2.10"):
|
|
4
|
+
from ckan.types import Context
|
|
5
|
+
else:
|
|
6
|
+
|
|
7
|
+
class Context(dict):
|
|
8
|
+
def __init__(self, **kwargs):
|
|
9
|
+
super().__init__(**kwargs)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import datetime
|
|
13
|
+
import itertools
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import ckanapi
|
|
19
|
+
import sqlalchemy as sa
|
|
20
|
+
from ckan import model
|
|
21
|
+
from ckan.lib.jobs import DEFAULT_QUEUE_NAME
|
|
22
|
+
from dateutil.parser import isoparse as parse_iso_date
|
|
23
|
+
from dateutil.parser import parse as parse_date
|
|
24
|
+
|
|
25
|
+
from ckanext.csvwmapandtransform import db, mapper
|
|
26
|
+
from ckanext.csvwmapandtransform.tasks import transform
|
|
27
|
+
|
|
28
|
+
log = __import__("logging").getLogger(__name__)
|
|
29
|
+
# must be lower case alphanumeric and these symbols: -_
|
|
30
|
+
MAPPING_GROUP = "mappings"
|
|
31
|
+
METHOD_GROUP = "methods"
|
|
32
|
+
JOB_TIMEOUT = 180
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def find_first_matching_id(dicts: list, key: str, value: str):
|
|
36
|
+
return next((d["id"] for d in dicts if d.get(key) == value), None)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def csvwmapandtransform_find_mappings(context: Context, data_dict):
|
|
40
|
+
mapping_group_id = find_first_matching_id(
|
|
41
|
+
toolkit.get_action("group_list")({}, {"all_fields": True}),
|
|
42
|
+
key="name",
|
|
43
|
+
value=MAPPING_GROUP,
|
|
44
|
+
)
|
|
45
|
+
if mapping_group_id:
|
|
46
|
+
mapping_group = toolkit.get_action("group_show")(
|
|
47
|
+
{"ignore_auth": True}, {"id": mapping_group_id, "include_datasets": True}
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
log.warn("group with name mappings not found!")
|
|
51
|
+
mapping_group = create_group(MAPPING_GROUP)
|
|
52
|
+
log.info("created group mappings")
|
|
53
|
+
packages = mapping_group.get("packages", None)
|
|
54
|
+
|
|
55
|
+
if packages:
|
|
56
|
+
packages = [
|
|
57
|
+
toolkit.get_action("package_show")({}, {"id": package["id"]})
|
|
58
|
+
for package in packages
|
|
59
|
+
]
|
|
60
|
+
resources = list(
|
|
61
|
+
itertools.chain.from_iterable(
|
|
62
|
+
[package["resources"] for package in packages]
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
resources = list()
|
|
67
|
+
return resources
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def csvwmapandtransform_test_mappings(context: Context, data_dict):
|
|
71
|
+
data_url = data_dict.get("data_url", None)
|
|
72
|
+
map_urls = data_dict.get("map_urls", None)
|
|
73
|
+
if not map_urls:
|
|
74
|
+
msg = {"map_urls": ["this field is mandatory."]}
|
|
75
|
+
raise toolkit.ValidationError(msg)
|
|
76
|
+
elif not data_url:
|
|
77
|
+
msg = {"data_url": ["this field is mandatory."]}
|
|
78
|
+
raise toolkit.ValidationError(msg)
|
|
79
|
+
tests = [mapper.check_mapping(map_url=url, data_url=data_url) for url in map_urls]
|
|
80
|
+
return tests
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def csvwmapandtransform_transform(
|
|
84
|
+
context: Context, data_dict: dict[str, Any]
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
"""Start a the transformation job for a certain resource.
|
|
87
|
+
|
|
88
|
+
:param resource_id: The resource id of the resource that you want the
|
|
89
|
+
datapusher status for.
|
|
90
|
+
:type resource_id: string
|
|
91
|
+
"""
|
|
92
|
+
if "id" in data_dict:
|
|
93
|
+
data_dict["resource_id"] = data_dict["id"]
|
|
94
|
+
res_id = toolkit.get_or_bust(data_dict, "resource_id")
|
|
95
|
+
resource = toolkit.get_action("resource_show")(
|
|
96
|
+
{"ignore_auth": True}, {"id": res_id}
|
|
97
|
+
)
|
|
98
|
+
data_dict["resource"] = resource
|
|
99
|
+
toolkit.check_access("csvwmapandtransform_transform", context, data_dict)
|
|
100
|
+
|
|
101
|
+
log.debug("transform_started for: {}".format(resource))
|
|
102
|
+
res = enqueue_transform(
|
|
103
|
+
resource["id"],
|
|
104
|
+
resource["name"],
|
|
105
|
+
resource["url"],
|
|
106
|
+
resource["package_id"],
|
|
107
|
+
operation="changed",
|
|
108
|
+
)
|
|
109
|
+
return res
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@toolkit.side_effect_free
|
|
113
|
+
def csvwmapandtransform_transform_status(
|
|
114
|
+
context: Context, data_dict: dict[str, Any]
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""Get the status of a the transformation job for a certain resource.
|
|
117
|
+
|
|
118
|
+
:param resource_id: The resource id of the resource that you want the
|
|
119
|
+
datapusher status for.
|
|
120
|
+
:type resource_id: string
|
|
121
|
+
"""
|
|
122
|
+
toolkit.check_access("csvwmapandtransform_transform_status", context, data_dict)
|
|
123
|
+
|
|
124
|
+
if "id" in data_dict:
|
|
125
|
+
data_dict["resource_id"] = data_dict["id"]
|
|
126
|
+
res_id = toolkit.get_or_bust(data_dict, "resource_id")
|
|
127
|
+
job_id = None
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
task = toolkit.get_action("task_status_show")(
|
|
131
|
+
{},
|
|
132
|
+
{
|
|
133
|
+
"entity_id": res_id,
|
|
134
|
+
"task_type": "csvwmapandtransform",
|
|
135
|
+
"key": "csvwmapandtransform",
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
except:
|
|
139
|
+
status = None
|
|
140
|
+
else:
|
|
141
|
+
value = json.loads(task["value"])
|
|
142
|
+
job_id = value.get("job_id")
|
|
143
|
+
url = None
|
|
144
|
+
job_detail = None
|
|
145
|
+
try:
|
|
146
|
+
error = json.loads(task["error"])
|
|
147
|
+
except ValueError:
|
|
148
|
+
# this happens occasionally, such as when the job times out
|
|
149
|
+
error = task["error"]
|
|
150
|
+
status = {
|
|
151
|
+
"status": task["state"],
|
|
152
|
+
"job_id": job_id,
|
|
153
|
+
"job_url": url,
|
|
154
|
+
"last_updated": task["last_updated"],
|
|
155
|
+
"error": error,
|
|
156
|
+
}
|
|
157
|
+
if job_id:
|
|
158
|
+
# get logs from db
|
|
159
|
+
db.init()
|
|
160
|
+
db_job = db.get_job(job_id)
|
|
161
|
+
|
|
162
|
+
if db_job and db_job.get("logs"):
|
|
163
|
+
for log in db_job["logs"]:
|
|
164
|
+
if "timestamp" in log and isinstance(
|
|
165
|
+
log["timestamp"], datetime.datetime
|
|
166
|
+
):
|
|
167
|
+
log["timestamp"] = log["timestamp"].isoformat()
|
|
168
|
+
status = dict(status, **db_job)
|
|
169
|
+
# status['task_info']=db_job
|
|
170
|
+
return status
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_actions():
|
|
174
|
+
actions = {
|
|
175
|
+
"csvwmapandtransform_find_mappings": csvwmapandtransform_find_mappings,
|
|
176
|
+
"csvwmapandtransform_transform": csvwmapandtransform_transform,
|
|
177
|
+
"csvwmapandtransform_test_mappings": csvwmapandtransform_test_mappings,
|
|
178
|
+
"csvwmapandtransform_transform_status": csvwmapandtransform_transform_status,
|
|
179
|
+
"csvwmapandtransform_hook": csvwmapandtransform_hook,
|
|
180
|
+
}
|
|
181
|
+
return actions
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def create_group(name):
|
|
185
|
+
local_ckan = ckanapi.LocalCKAN()
|
|
186
|
+
group = local_ckan.action.group_create(name=name)
|
|
187
|
+
return group
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def enqueue_transform(res_id, res_name, res_url, dataset_id, operation):
|
|
191
|
+
# skip task if the dataset is already queued
|
|
192
|
+
queue = DEFAULT_QUEUE_NAME
|
|
193
|
+
# jobs = toolkit.get_action("job_list")({"ignore_auth": True}, {"queues": [queue]})
|
|
194
|
+
# log.debug("test-jobs")
|
|
195
|
+
# log.debug(jobs)
|
|
196
|
+
# Check if this resource is already in the process of being xloadered
|
|
197
|
+
task = {
|
|
198
|
+
"entity_id": res_id,
|
|
199
|
+
"entity_type": "resource",
|
|
200
|
+
"task_type": "csvwmapandtransform",
|
|
201
|
+
"last_updated": str(datetime.datetime.utcnow()),
|
|
202
|
+
"state": "submitting",
|
|
203
|
+
"key": "csvwmapandtransform",
|
|
204
|
+
"value": "{}",
|
|
205
|
+
"error": "{}",
|
|
206
|
+
"detail": "",
|
|
207
|
+
}
|
|
208
|
+
try:
|
|
209
|
+
existing_task = toolkit.get_action("task_status_show")(
|
|
210
|
+
{},
|
|
211
|
+
{
|
|
212
|
+
"entity_id": res_id,
|
|
213
|
+
"task_type": "csvwmapandtransform",
|
|
214
|
+
"key": "csvwmapandtransform",
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
assume_task_stale_after = datetime.timedelta(seconds=3600)
|
|
218
|
+
assume_task_stillborn_after = datetime.timedelta(seconds=int(5))
|
|
219
|
+
if existing_task.get("state") == "pending":
|
|
220
|
+
# queued_res_ids = [
|
|
221
|
+
# re.search(r"'resource_id': u?'([^']+)'",
|
|
222
|
+
# job.description).groups()[0]
|
|
223
|
+
# for job in get_queue().get_jobs()
|
|
224
|
+
# if 'xloader_to_datastore' in str(job) # filter out test_job etc
|
|
225
|
+
# ]
|
|
226
|
+
updated = parse_iso_date(existing_task["last_updated"])
|
|
227
|
+
time_since_last_updated = datetime.datetime.utcnow() - updated
|
|
228
|
+
# if (res_id not in queued_res_ids
|
|
229
|
+
# and time_since_last_updated > assume_task_stillborn_after):
|
|
230
|
+
# # it's not on the queue (and if it had just been started then
|
|
231
|
+
# # its taken too long to update the task_status from pending -
|
|
232
|
+
# # the first thing it should do in the xloader job).
|
|
233
|
+
# # Let it be restarted.
|
|
234
|
+
# log.info('A pending task was found %r, but its not found in '
|
|
235
|
+
# 'the queue %r and is %s hours old',
|
|
236
|
+
# existing_task['id'], queued_res_ids,
|
|
237
|
+
# time_since_last_updated)
|
|
238
|
+
# elif time_since_last_updated > assume_task_stale_after:
|
|
239
|
+
if time_since_last_updated > assume_task_stale_after:
|
|
240
|
+
# it's been a while since the job was last updated - it's more
|
|
241
|
+
# likely something went wrong with it and the state wasn't
|
|
242
|
+
# updated than its still in progress. Let it be restarted.
|
|
243
|
+
log.info(
|
|
244
|
+
"A pending task was found %r, but it is only %s hours" " old",
|
|
245
|
+
existing_task["id"],
|
|
246
|
+
time_since_last_updated,
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
log.info(
|
|
250
|
+
"A pending task was found %s for this resource, so "
|
|
251
|
+
"skipping this duplicate task",
|
|
252
|
+
existing_task["id"],
|
|
253
|
+
)
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
task["id"] = existing_task["id"]
|
|
257
|
+
except toolkit.ObjectNotFound:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
callback_url = toolkit.url_for(
|
|
261
|
+
"api.action", ver=3, logic_function="csvwmapandtransform_hook", qualified=True
|
|
262
|
+
)
|
|
263
|
+
# initioalize database for additional job data
|
|
264
|
+
db.init()
|
|
265
|
+
# Store details of the job in the db
|
|
266
|
+
|
|
267
|
+
# add this dataset to the queue
|
|
268
|
+
job = toolkit.enqueue_job(
|
|
269
|
+
transform,
|
|
270
|
+
[res_url, res_id, dataset_id, callback_url, task["last_updated"]],
|
|
271
|
+
title='csvwmapandtransform {} "{}" {}'.format(operation, res_name, res_url),
|
|
272
|
+
queue=queue, # , timeout=JOB_TIMEOUT
|
|
273
|
+
)
|
|
274
|
+
try:
|
|
275
|
+
db.add_pending_job(job.id, job_type=task["task_type"], result_url=callback_url)
|
|
276
|
+
except sa.exc.IntegrityError:
|
|
277
|
+
raise Exception("job_id {} already exists".format(task["id"]))
|
|
278
|
+
|
|
279
|
+
# log.info("added a job to csvwmapandtransform database")
|
|
280
|
+
# log.debug("enqueued job id".format(job.id)
|
|
281
|
+
|
|
282
|
+
log.debug("Enqueued job {} to {} resource {}".format(job.id, operation, res_name))
|
|
283
|
+
|
|
284
|
+
value = json.dumps({"job_id": job.id})
|
|
285
|
+
task["value"] = value
|
|
286
|
+
task["state"] = "pending"
|
|
287
|
+
task["last_updated"] = str(datetime.datetime.utcnow())
|
|
288
|
+
toolkit.get_action("task_status_update")(
|
|
289
|
+
{"session": model.meta.create_local_session(), "ignore_auth": True}, task
|
|
290
|
+
)
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def csvwmapandtransform_hook(context, data_dict):
|
|
295
|
+
"""Update csvwmapandtransform task. This action is typically called by ckanext-csvwmapandtransform
|
|
296
|
+
whenever the status of a job changes.
|
|
297
|
+
|
|
298
|
+
:param metadata: metadata provided when submitting job. key-value pairs.
|
|
299
|
+
Must have resource_id property.
|
|
300
|
+
:type metadata: dict
|
|
301
|
+
:param status: status of the job from the csvwmapandtransform service. Allowed values:
|
|
302
|
+
pending, running, running_but_viewable, complete, error
|
|
303
|
+
(which must all be valid values for task_status too)
|
|
304
|
+
:type status: string
|
|
305
|
+
:param error: Error raised during job execution
|
|
306
|
+
:type error: string
|
|
307
|
+
|
|
308
|
+
NB here are other params which are in the equivalent object in
|
|
309
|
+
ckan-service-provider (from job_status):
|
|
310
|
+
:param sent_data: Input data for job
|
|
311
|
+
:type sent_data: json encodable data
|
|
312
|
+
:param job_id: An identifier for the job
|
|
313
|
+
:type job_id: string
|
|
314
|
+
:param result_url: Callback url
|
|
315
|
+
:type result_url: url string
|
|
316
|
+
:param data: Results from job.
|
|
317
|
+
:type data: json encodable data
|
|
318
|
+
:param requested_timestamp: Time the job started
|
|
319
|
+
:type requested_timestamp: timestamp
|
|
320
|
+
:param finished_timestamp: Time the job finished
|
|
321
|
+
:type finished_timestamp: timestamp
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
metadata, status, job_info = toolkit.get_or_bust(
|
|
326
|
+
data_dict, ["metadata", "status", "job_info"]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
res_id = toolkit.get_or_bust(metadata, "resource_id")
|
|
330
|
+
|
|
331
|
+
# Pass metadata, not data_dict, as it contains the resource id needed
|
|
332
|
+
# on the auth checks
|
|
333
|
+
# toolkit.check_access('xloader_submit', context, metadata)
|
|
334
|
+
|
|
335
|
+
task = toolkit.get_action("task_status_show")(
|
|
336
|
+
context,
|
|
337
|
+
{
|
|
338
|
+
"entity_id": res_id,
|
|
339
|
+
"task_type": "csvwmapandtransform",
|
|
340
|
+
"key": "csvwmapandtransform",
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
task["state"] = status
|
|
345
|
+
task["last_updated"] = str(datetime.datetime.utcnow())
|
|
346
|
+
task["error"] = data_dict.get("error")
|
|
347
|
+
# task['task_info'] = job_info
|
|
348
|
+
resubmit = False
|
|
349
|
+
|
|
350
|
+
if status in ("complete", "running_but_viewable"):
|
|
351
|
+
# Create default views for resource if necessary (only the ones that
|
|
352
|
+
# require data to be in the DataStore)
|
|
353
|
+
resource_dict = toolkit.get_action("resource_show")(context, {"id": res_id})
|
|
354
|
+
|
|
355
|
+
dataset_dict = toolkit.get_action("package_show")(
|
|
356
|
+
context, {"id": resource_dict["package_id"]}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Check if the uploaded file has been modified in the meantime
|
|
360
|
+
if resource_dict.get("last_modified") and metadata.get("task_created"):
|
|
361
|
+
try:
|
|
362
|
+
last_modified_datetime = parse_date(resource_dict["last_modified"])
|
|
363
|
+
task_created_datetime = parse_date(metadata["task_created"])
|
|
364
|
+
if last_modified_datetime > task_created_datetime:
|
|
365
|
+
log.debug(
|
|
366
|
+
"Uploaded file more recent: %s > %s",
|
|
367
|
+
last_modified_datetime,
|
|
368
|
+
task_created_datetime,
|
|
369
|
+
)
|
|
370
|
+
resubmit = True
|
|
371
|
+
except ValueError:
|
|
372
|
+
pass
|
|
373
|
+
# Check if the URL of the file has been modified in the meantime
|
|
374
|
+
elif (
|
|
375
|
+
resource_dict.get("url")
|
|
376
|
+
and metadata.get("original_url")
|
|
377
|
+
and resource_dict["url"] != metadata["original_url"]
|
|
378
|
+
):
|
|
379
|
+
log.debug(
|
|
380
|
+
"URLs are different: %s != %s",
|
|
381
|
+
resource_dict["url"],
|
|
382
|
+
metadata["original_url"],
|
|
383
|
+
)
|
|
384
|
+
resubmit = True
|
|
385
|
+
# mark job completed in db
|
|
386
|
+
log.debug(task)
|
|
387
|
+
log.debug(job_info)
|
|
388
|
+
|
|
389
|
+
if status == "complete":
|
|
390
|
+
log.debug("job complete now update job db at: {}".format(task))
|
|
391
|
+
db.init()
|
|
392
|
+
job_id = json.loads(task["value"])["job_id"]
|
|
393
|
+
db.mark_job_as_completed(job_id)
|
|
394
|
+
|
|
395
|
+
context["ignore_auth"] = True
|
|
396
|
+
toolkit.get_action("task_status_update")(context, task)
|
|
397
|
+
|
|
398
|
+
if resubmit:
|
|
399
|
+
log.debug(
|
|
400
|
+
"Resource %s has been modified, " "resubmitting to csvwmapandtransform",
|
|
401
|
+
res_id,
|
|
402
|
+
)
|
|
403
|
+
toolkit.get_action("csvwmapandtransform_transform")(
|
|
404
|
+
context, {"resource_id": res_id}
|
|
405
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
ckan.module('csvwmapandtransform', function (jQuery) {
|
|
2
|
+
return {
|
|
3
|
+
options: {
|
|
4
|
+
parameters: {
|
|
5
|
+
html: {
|
|
6
|
+
contentType: 'application/json', // change the content type to text/html
|
|
7
|
+
dataType: 'json', // change the data type to html
|
|
8
|
+
dataConverter: function (data) { return data; },
|
|
9
|
+
language: 'json'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
initialize: function () {
|
|
14
|
+
var self = this;
|
|
15
|
+
var p;
|
|
16
|
+
p = this.options.parameters.html;
|
|
17
|
+
console.log("Initialized csvwmapandtransform for element: ", this.el);
|
|
18
|
+
const apiUrl = this.el.get(0).getAttribute("data-api-url"); // Fetch the API URL from the parent element
|
|
19
|
+
if (!apiUrl) {
|
|
20
|
+
console.error("No API URL specified for counter:", this.el);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
var log_length;
|
|
24
|
+
log_length = 0;
|
|
25
|
+
var update = function () { // define the update function
|
|
26
|
+
jQuery.ajax({
|
|
27
|
+
url: apiUrl,
|
|
28
|
+
type: 'GET',
|
|
29
|
+
contentType: p.contentType,
|
|
30
|
+
dataType: p.dataType,
|
|
31
|
+
data: { get_param: 'value' },
|
|
32
|
+
success: function (data) {
|
|
33
|
+
const haslogs = 'logs' in data.status;
|
|
34
|
+
const hasresults = data.status?.data?.files;
|
|
35
|
+
if (hasresults || haslogs) {
|
|
36
|
+
// console.log(self.el.find('button[name="delete"]'));
|
|
37
|
+
self.el.find('button[name="delete"]').removeClass("invisible");
|
|
38
|
+
self.el.find('div[name="status"]').removeClass("invisible");
|
|
39
|
+
};
|
|
40
|
+
// console.log(haslogs, hasresults);
|
|
41
|
+
if (!haslogs) return;
|
|
42
|
+
var length = Object.keys(data.status.logs).length;
|
|
43
|
+
if (length) {
|
|
44
|
+
if (length !== log_length) {
|
|
45
|
+
// self.el.html(JSON.stringify(data, null, 2)); // update the HTML if there are changes
|
|
46
|
+
var logs_div = $(self.el).find('ul[name="log"]');
|
|
47
|
+
jQuery.each(data.status.logs, function (key, value) {
|
|
48
|
+
if (key + 1 < log_length) return;
|
|
49
|
+
logs_div.append("<li class='item "
|
|
50
|
+
+ value.class +
|
|
51
|
+
"'><i class='fa icon fa-"
|
|
52
|
+
+ value.icon +
|
|
53
|
+
"'></i><div class='alert alert-"
|
|
54
|
+
+ value.alertlevel +
|
|
55
|
+
" mb-0 mt-3' role='alert'>"
|
|
56
|
+
+ value.message +
|
|
57
|
+
"</div><span class='date' title='timestamp'>"
|
|
58
|
+
+ value.timestamp +
|
|
59
|
+
"</span></li>");
|
|
60
|
+
console.log("Appending log:", value);
|
|
61
|
+
});
|
|
62
|
+
console.log("csvwmapandtransform: status updated");
|
|
63
|
+
log_length = length;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// console.log('no log changes');
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
error: function (xhr, status, error) {
|
|
70
|
+
console.log('Error:', error);
|
|
71
|
+
},
|
|
72
|
+
complete: function () {
|
|
73
|
+
// call the update function recursively after a delay
|
|
74
|
+
setTimeout(update, 2000);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
update(); // call the update function immediately after initialization
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
.activity {
|
|
2
|
+
padding: 0;
|
|
3
|
+
list-style-type: none;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.activity .item {
|
|
7
|
+
position: relative;
|
|
8
|
+
margin: 0 0 15px 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.activity .item .user-image {
|
|
13
|
+
border-radius: 100px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.activity .item .date {
|
|
17
|
+
color: #999999;
|
|
18
|
+
font-size: 12px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.activity .item.no-avatar p {
|
|
22
|
+
margin-left: 40px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.activity.activity-empty {
|
|
26
|
+
background: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@media (min-width: 768px) {
|
|
31
|
+
.activity {
|
|
32
|
+
background: transparent url("/dotted.png") 14px 0 repeat-y;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.activity .item .date {
|
|
36
|
+
margin: 5px 0 0 80px;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.activity .item .icon {
|
|
41
|
+
display: block;
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: 0;
|
|
44
|
+
left: 0;
|
|
45
|
+
width: 30px;
|
|
46
|
+
height: 30px;
|
|
47
|
+
line-height: 30px;
|
|
48
|
+
text-align: center;
|
|
49
|
+
color: #FFFFFF;
|
|
50
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
51
|
+
/* font-weight: normal; */
|
|
52
|
+
margin-right: 10px;
|
|
53
|
+
border-radius: 100px;
|
|
54
|
+
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
55
|
+
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.activity .item .alert {
|
|
59
|
+
margin: 5px 0 0 40px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.activity_buttons {
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.activity_buttons>a.btn.disabled {
|
|
68
|
+
color: inherit;
|
|
69
|
+
opacity: 0.7;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.popover {
|
|
73
|
+
width: 300px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.popover .popover-title {
|
|
77
|
+
font-weight: bold;
|
|
78
|
+
margin-bottom: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.popover p.about {
|
|
82
|
+
margin: 0 0 10px 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.popover .popover-close {
|
|
86
|
+
float: right;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.popover .empty {
|
|
91
|
+
padding: 10px;
|
|
92
|
+
color: #6E6E6E;
|
|
93
|
+
font-style: italic;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
.activity .item p {
|
|
98
|
+
font-size: 14px;
|
|
99
|
+
line-height: 1.5;
|
|
100
|
+
margin: 5px 0 0 40px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.activity .item.failure .icon {
|
|
104
|
+
background: #B95252;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.activity .item.success .icon {
|
|
108
|
+
background: #69A67A;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.select-time {
|
|
112
|
+
width: 250px;
|
|
113
|
+
display: inline;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
br.line-height2 {
|
|
117
|
+
line-height: 2;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.pull-right {
|
|
121
|
+
float: right;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*# sourceMappingURL=activity.css.map */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from ckan.logic.auth.get import task_status_show
|
|
2
|
+
|
|
3
|
+
import ckanext.datastore.logic.auth as auth
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def csvwmapandtransform_transform(context, data_dict):
|
|
7
|
+
if "resource" in data_dict and data_dict["resource"].get("package_id"):
|
|
8
|
+
data_dict["id"] = data_dict["resource"].get("package_id")
|
|
9
|
+
privilege = "package_update"
|
|
10
|
+
else:
|
|
11
|
+
privilege = "resource_update"
|
|
12
|
+
return auth.datastore_auth(context, data_dict, privilege=privilege)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def csvwmapandtransform_transform_status(context, data_dict):
|
|
16
|
+
return task_status_show(context, data_dict)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_auth_functions():
|
|
20
|
+
return {
|
|
21
|
+
"csvwmapandtransform_transform": csvwmapandtransform_transform,
|
|
22
|
+
"csvwmapandtransform_transform_status": csvwmapandtransform_transform_status,
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@click.group(short_help="csvwmapandtransform CLI.")
|
|
5
|
+
def csvwmapandtransform():
|
|
6
|
+
"""csvwmapandtransform CLI."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@csvwmapandtransform.command()
|
|
11
|
+
@click.argument("name", default="csvwmapandtransform")
|
|
12
|
+
def command(name):
|
|
13
|
+
"""Docs."""
|
|
14
|
+
click.echo("Hello, {name}!".format(name=name))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_commands():
|
|
18
|
+
return [csvwmapandtransform]
|