This repository has been archived by the owner on Jun 19, 2023. It is now read-only.
/
plugin.py
698 lines (578 loc) · 29.3 KB
/
plugin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
from logging import getLogger
from ckan.lib.helpers import flash_notice
import ckan.plugins as p
import ckan.plugins.toolkit as toolkit
from ckanext.dgu.authentication.drupal_auth import DrupalAuthMiddleware
from ckanext.dgu.lib.site_down_middleware import SiteDownMiddleware
from ckanext.dgu.authorize import (
dgu_package_update,
dgu_extra_fields_editable,
dgu_dataset_delete, dgu_user_list, dgu_user_show,
dgu_organization_delete, dgu_group_change_state,
)
from ckanext.report.interfaces import IReport
from ckan.lib.helpers import url_for
from ckanext.dgu.lib.helpers import dgu_linked_user, is_plugin_enabled
from ckanext.dgu.search_indexing import SearchIndexing
from ckanext.dgu import gemini_postprocess_tasks
from ckan.config.routing import SubMapper
from ckan.exceptions import CkanUrlException
log = getLogger(__name__)
def after(instance, action, **params):
from pylons import response
instance._set_cors()
response.headers['Vary'] = 'Cookie'
def not_found(self, url):
from ckan.lib.base import abort
abort(404)
def _guess_package_type(self, expecting_name=False):
return 'dataset'
def delete_routes_by_path_startswtih(map, path_startswith):
matches_to_delete = []
for match in map.matchlist:
if match.routepath.startswith(path_startswith):
matches_to_delete.append(match)
for match in matches_to_delete:
map.matchlist.remove(match)
def delete_routes_by_name(map, route_names):
if isinstance(route_names, basestring):
route_names = [route_names]
for route_name in route_names:
del map._routenames[route_name]
class DguReportPlugin(p.SingletonPlugin):
p.implements(p.IRoutes, inherit=True)
def after_map(self, map):
# Put reports at /data/reports instead of /reports
# Delete routes to /report otherwise url_for links end up pointed there.
delete_routes_by_path_startswtih(map, '/report')
delete_routes_by_name(map, ('reports', 'report', 'report-org'))
# Add new routes to /data/reports
report_ctlr = 'ckanext.report.controllers:ReportController'
map.connect('reports', '/data/report', controller=report_ctlr, action='index')
map.redirect('/data/reports', '/data/report')
map.connect('report', '/data/report/:report_name', controller=report_ctlr, action='view')
map.connect('report-org', '/data/report/:report_name/:organization', controller=report_ctlr, action='view')
# Commitment reports
c_ctlr = 'ckanext.dgu.controllers.commitment:CommitmentController'
map.connect('commitments', '/data/reports/commitments',
controller=c_ctlr, action='index')
map.connect('commitments_publisher', '/data/reports/commitments/:id',
controller=c_ctlr, action='commitments')
map.connect('commitments_edit', '/data/reports/commitments/:id/edit',
controller=c_ctlr, action='edit')
# Redirects for changed report locations May 2014
map.redirect('/data/reports/qa/organisation/broken_resource_links',
'/data/report/broken-links')
map.redirect('/data/reports/qa/organisation/broken_resource_links/:organization',
'/data/report/broken-links/:organization')
map.redirect('/data/reports/resources/:organization',
'/data/report/publisher-resources/:organization')
# openness done
map.redirect('/data/reports/qa',
'/data/report')
map.redirect('/data/reports/qa/organisation/:organization',
'/data/report/openness/:organization')
map.redirect('/api/2/util/qa/{a:.*}',
'/data/report')
map.redirect('/data/reports/qa/organisation/scores',
'/data/report/openness')
map.redirect('/data/reports/qa/organisation/scores/:organization',
'/data/report/openness/:organization')
# commitments - keep in existing controller
# /data/reports/commitments
# /data/reports/commitments/cabinet-office
# site usage - keep as it is
# Other misc:
# /publisher/report_publishers_and_users
# /publisher/report_groups_without_admins
# /publisher/report_users_not_assigned_to_groups
return map
class ThemePlugin(p.SingletonPlugin):
'''
DGU Visual Theme for a CKAN install embedded in dgu/Drupal.
'''
p.implements(p.IConfigurer)
p.implements(p.IRoutes, inherit=True)
p.implements(p.ITemplateHelpers, inherit=True)
from ckan.lib.base import h, BaseController
# [Monkey patch] Replace h.linked_user with a version to hide usernames
h.linked_user = dgu_linked_user
# [Monkey patch] Replace BaseController.__after__ to allow us to add more
# headers for caching
BaseController.__after__ = after
# [Monkey patch] Replace TemplateController.view since it isn't used
# in normal use. Hack attempts sometimes get through to it though
# and there is no need to attempt to barf on their unicode characters
from ckan.controllers.template import TemplateController
TemplateController.view = not_found
# [Monkey patch] Stop autodetecting package-type by the URL since it seems
# to think /data/search should search for 'search' packages!
from ckan.controllers.package import PackageController
PackageController._guess_package_type = _guess_package_type
# [Monkey patch] We have complicated UKLP license info, so need a custom
# isopen method
from ckan import model
from lib import helpers as dgu_helpers
model.Package._isopen = model.Package.isopen
model.Package.isopen = dgu_helpers.isopen
def update_config(self, config):
toolkit.add_template_directory(config, 'theme/templates')
toolkit.add_public_directory(config, 'theme/public')
# Shared assets may be configured to be elsewhere on disk,
# so in that case let the user configure apache/nginx to serve
# them manually. But for developers, the shared assets will
# simply next door to this repo, dgu.shared_assets_timestamp_path
# won't be set, so as a convenience we get paster to serve them.
if not config.get('dgu.shared_assets_timestamp_path'):
toolkit.add_public_directory(config, '../../../shared_dguk_assets')
def get_helpers(self):
"""
A dictionary of extra helpers that will be available to provide
dgu specific helpers to the templates. We may be able to override
h.linked_user so that we don't need to monkey patch above.
"""
from ckanext.dgu.lib import helpers
from inspect import getmembers, isfunction
helper_dict = {}
functions_list = [o for o in getmembers(helpers, isfunction)]
for name, fn in functions_list:
if name[0] != '_':
helper_dict[name] = fn
return helper_dict
def before_map(self, map):
"""
Make "/data" the homepage.
"""
data_controller = 'ckanext.dgu.controllers.data:DataController'
user_controller = 'ckanext.dgu.controllers.user:UserController'
map.redirect('/data', '/data/search')
#map.connect('/data', controller=data_controller, action='index')
map.connect('/linked-data-admin', controller=data_controller, action='linked_data_admin')
map.connect('dgu_search', '/data/search', controller='package', action='search')
map.connect('api_page', '/data/metadata-api-docs', controller=data_controller, action='api')
map.connect('system_dashboard', '/data/system_dashboard', controller=data_controller, action='system_dashboard')
map.connect('openspending_index', '/data/openspending-report/index', controller=data_controller, action='openspending_report')
map.connect('openspending_read', '/data/openspending-report/{id}', controller=data_controller, action='openspending_publisher_report')
map.connect('/data/resource_cache/{root}/{resource_id}/{filename}', controller=data_controller, action='resource_cache')
map.connect('/data/viz/social-investment-and-foundations', controller=data_controller, action='viz_social_investment_and_foundations')
map.connect('/data/viz/investment-readiness-programme', controller=data_controller, action='viz_investment_readiness_programme')
map.connect('/data/viz/upload', controller=data_controller, action='viz_upload')
map.connect('/', controller=data_controller, action='home')
map.connect('/data/contracts-finder-archive{relative_url:.*}', controller=data_controller, action='contracts_archive')
theme_controller = 'ckanext.dgu.controllers.theme:ThemeController'
map.connect('/data/themes', controller=theme_controller, action='index')
map.connect('/data/themes/{name}', controller=theme_controller, action='named_theme')
map.connect('/data/random-datasets', controller=data_controller, action='random_datasets')
# For test usage when Drupal is not running
map.connect('/comment/get/{id}',
controller='ckanext.dgu.controllers.package:CommentProxy',
action='get_comments')
# Remap the /user/me to the DGU version of the User controller
with SubMapper(map, controller=user_controller) as m:
m.connect('/data/user/me', action='me')
# Map /user* to /data/user/ because Drupal uses /user
with SubMapper(map, controller='user') as m:
m.connect('/data/user/edit', action='edit')
m.connect('/data/user/edit/{id:.*}', action='edit')
m.connect('/data/user/reset/{id:.*}', action='perform_reset')
m.connect('/data/user/register', action='register')
m.connect('/data/user/login', action='login')
m.connect('/data/user/_logout', action='logout')
m.connect('/data/user/logged_in', action='logged_in')
m.connect('/data/user/logged_out', action='logged_out')
m.connect('/data/user/logged_out_redirect', action='logged_out_page')
m.connect('/data/user/reset', action='request_reset')
#NB not /data/user/me
m.connect('/data/user/set_lang/{lang}', action='set_lang')
m.connect('user_read', '/data/user/{id:.*}', action='read')
m.connect('/data/user', action='index')
map.redirect('/dashboard', '/data/user/me')
return map
def after_map(self, map):
# Delete routes to /tag since we use /data/tag and /tag is confusing to
# have kicking around still when it uses the same template with
# different inputs.
delete_routes_by_path_startswtih(map, path_startswith='/tag')
return map
class DrupalAuthPlugin(p.SingletonPlugin):
'''Reads Drupal login cookies to log user in.'''
p.implements(p.IMiddleware, inherit=True)
def make_middleware(self, app, config):
return DrupalAuthMiddleware(app, config)
class AuthApiPlugin(p.SingletonPlugin):
'''Adds functions that work out if the user is allowed to do
certain edits.'''
p.implements(p.IAuthFunctions, inherit=True)
def get_auth_functions(self):
return {
'package_update': dgu_package_update,
'package_extra_fields_editable': dgu_extra_fields_editable,
'package_delete': dgu_dataset_delete,
'user_list': dgu_user_list,
'user_show': dgu_user_show,
'organization_delete': dgu_organization_delete,
'group_change_state': dgu_group_change_state,
}
class DguForm(p.SingletonPlugin):
# NB the actual form (IDatasetForm) is in forms/dataset_form.py
p.implements(p.IRoutes, inherit=True)
# IRoutes
def before_map(self, map):
dgu_package_controller = 'ckanext.dgu.controllers.package:PackageController'
map.connect('dataset_new', '/dataset/new', controller=dgu_package_controller, action='new')
map.connect('dataset_edit', '/dataset/edit/{id}', controller=dgu_package_controller, action='edit')
map.connect('/dataset/delete/{id}', controller=dgu_package_controller, action='delete')
map.connect('dataset_history', '/dataset/history/{id}', controller=dgu_package_controller, action='history')
map.connect('/dataset/{id}.{format}', controller=dgu_package_controller, action='read')
map.connect('/dataset/{id}', controller=dgu_package_controller, action='read')
map.connect('/dataset/{id}/resource/{resource_id}', controller=dgu_package_controller, action='resource_read')
return map
class PublisherPlugin(p.SingletonPlugin):
p.implements(p.IRoutes, inherit=True)
p.implements(p.ISession, inherit=True)
p.implements(IReport)
def before_commit(self, session):
"""
Before we commit a session we will check to see if any of the new
items are users so we can notify them to apply for publisher access.
"""
from pylons.i18n import _
from ckan.model import User
session.flush()
if not hasattr(session, '_object_cache'):
return
pubctlr = 'ckanext.dgu.controllers.publisher:PublisherController'
for obj in set(session._object_cache['new']):
if isinstance(obj, (User)):
try:
url = url_for(controller=pubctlr, action='apply')
except CkanUrlException:
# This occurs when Routes has not yet been initialized
# yet, which would be before a WSGI request has been
# made. In this case, there will be no flash message
# required anyway.
return
msg = "You can now <a href='%s'>apply for publisher access</a>" % url
try:
flash_notice(_(msg), allow_html=True)
except TypeError:
# Raised when there is no session registered, and this is
# the case when using the paster commands.
#log.debug('Did not add a flash message due to a missing session: %s' % msg)
pass
def before_map(self, map):
map.redirect('/organization/{url:.*}', '/publisher/{url}')
with SubMapper(map, controller='ckanext.dgu.controllers.publisher:PublisherController') as m:
m.connect('publisher_index',
'/publisher', action='index')
m.connect('publisher_edit',
'/publisher/edit/:id', action='edit')
m.connect('publisher_apply',
'/publisher/apply/:id', action='apply')
m.connect('publisher_apply_empty',
'/publisher/apply', action='apply')
m.connect('publisher_requests',
'/publisher/users/requests', action='publisher_requests')
m.connect('publisher_request',
'/publisher/users/request/:token', action='publisher_request')
m.connect('publisher_request_decision',
'/publisher/users/request/:token/:decision',
action='publisher_request')
m.connect('publisher_users',
'/publisher/users/:id', action='users')
m.connect('publisher_new',
'/publisher/new', action='new')
m.connect('/publisher/report_groups_without_admins',
action='report_groups_without_admins')
m.connect('/publisher/report_publishers_and_users',
action='report_publishers_and_users')
m.connect('/publisher/report_users',
action='report_users')
m.connect('/publisher/report_users_not_assigned_to_groups',
action='report_users_not_assigned_to_groups')
m.connect('publisher_read',
'/publisher/:id',
action='read')
return map
def after_map(self, map):
if is_plugin_enabled('issues'):
delete_routes_by_name(map, 'issues_for_organization')
with SubMapper(map, controller='ckanext.issues.controller:IssueController') as m:
m.connect('issues_for_organization', '/publisher/:org_id/issues', action='issues_for_organization')
return map
def update_config(self, config):
# set the auth profile to use the publisher based auth
config['ckan.auth.profile'] = 'publisher'
# same for the harvesting auth profile
config['ckan.harvest.auth.profile'] = 'publisher'
# IReport
def register_reports(self):
"""Register details of an extension's reports"""
from ckanext.dgu.lib import reports
return [reports.nii_report_info,
reports.publisher_activity_report_info,
reports.publisher_resources_info,
reports.unpublished_report_info,
reports.datasets_without_resources_info,
reports.app_dataset_theme_report_info,
reports.app_dataset_report_info,
reports.admin_editor_info,
reports.licence_report_info,
reports.la_schemas_info,
reports.pdf_datasets_report_info,
reports.html_datasets_report_info,
]
class InventoryPlugin(p.SingletonPlugin):
p.implements(p.IRoutes, inherit=True)
p.implements(p.IConfigurer)
p.implements(p.ISession, inherit=True)
def before_commit(self, session):
pass
def before_map(self, map):
inv_ctlr = 'ckanext.dgu.controllers.inventory:InventoryController'
map.connect('/unpublished/edit-item/:id',
controller=inv_ctlr, action='edit_item')
map.connect('unpublished_edit', '/unpublished/:id/edit',
controller=inv_ctlr, action='edit')
map.connect('/unpublished/:id/edit/download',
controller=inv_ctlr, action='download')
map.connect('/unpublished/:id/edit/template',
controller=inv_ctlr, action='template')
map.connect('/unpublished/:id/edit/upload',
controller=inv_ctlr, action='upload')
map.connect('/unpublished/:id/edit/upload_complete',
controller=inv_ctlr, action='upload_complete')
map.connect('/unpublished/:id/edit/upload/:upload_id',
controller=inv_ctlr, action='upload_status')
return map
def after_map(self, map):
return map
def update_config(self, config):
pass
class SearchPlugin(p.SingletonPlugin):
"""
DGU-specific searching.
One thing DGU specific about the search is that DGU facets on
whether a dataset's license_id is OGL (Open Government License) or not.
Since this is calculable from the license_id, but is not a facet over the
whole set of possible license_id values (ie. 'ukcrown', 'other' etc. should
all be grouped together under the 'non-ogl' facet), we index on a field
that doesn't exist on the dataset itself. See `SearchPlugin.before_index`.
Another thing that DGU does differently is that it cleans up the resource
formats prior to indexing.
A further thing that DGU does differently is to index the group title, as
well as the group name.
"""
p.implements(p.IPackageController, inherit=True)
def read(self, entity):
pass
def create(self, entity):
pass
def edit(self, entity):
pass
def authz_add_role(self, object_role):
pass
def authz_remove_role(self, object_role):
pass
def delete(self, entity):
pass
def before_search(self, search_params):
"""
Modify the search query.
"""
# Set the 'qf' (queryfield) parameter to a fixed list of boosted solr fields
# tuned for DGU. If a dismax query is run, then these will be the fields that are searched
# within.
search_params['qf'] = 'title^4 name^3 notes^2 text organization_titles^0.3 extras_harvest_document_content^0.2'
# boost NII datasets. used trial and error to get reasonable mix of useful NII ones
# on and relevant non-NII ones for /data/search?q=road and /data/search?q=crime
# Boost registers much higher as we know they are much higher quality datasets.
search_params['bf'] = 'core_dataset^20 register^100'
# ignore dataset_type:dataset which CKAN2 adds in - we dont use
# dataset_type and it mucks up spatial search
if search_params.get('fq'):
search_params['fq'] = search_params['fq'].replace('+dataset_type:dataset', '')
# If the user does not specify a "sort by" method manually,
# then it defaults here (and the UI has to have the same logic)
# NB The UI has to be kept in step with this logic:
# ckanext/dgu/theme/templates/package/search.html
order_by = search_params.get('sort')
bbox = search_params.get('extras', {}).get('ext_bbox')
search_params_apart_from_bbox = search_params.get('q', '') + search_params.get('fq', '')
sort_by_location_enabled = bool(bbox and not search_params_apart_from_bbox)
if order_by in (None, 'spatial desc') and sort_by_location_enabled:
search_params['sort'] = 'spatial desc'
# This sort parameter is picked up by ckanext-spatial
elif order_by in (None, 'rank'):
# Score = SOLR rank = relevancy = how well q keyword matches the
# SOLR record's text content.
# Add popularity into default ranking - this kicks when there is
# no keyword, so no rank. Leave name there, in case no popularity
# scores have been loaded.
search_params['sort'] = 'score desc, popularity desc, name asc'
return search_params
def after_search(self, search_results, search_params):
return search_results
def before_view(self, pkg_dict):
return pkg_dict
def before_index(self, pkg_dict):
"""
DGU-specific changes to the pkg_dict before it is indexed. The main
reason is so that we can add search facets.
"""
log.info('Indexing: %s', pkg_dict['name'])
SearchIndexing.clean_title_string(pkg_dict)
SearchIndexing.add_field__is_ogl(pkg_dict)
SearchIndexing.resource_format_cleanup(pkg_dict)
SearchIndexing.add_field__publisher(pkg_dict)
SearchIndexing.add_field__organization_title_and_abbreviation(pkg_dict)
if is_plugin_enabled('harvest'):
SearchIndexing.add_field__harvest_document(pkg_dict)
SearchIndexing.add_field__openness(pkg_dict)
SearchIndexing.add_popularity(pkg_dict)
SearchIndexing.add_inventory(pkg_dict)
SearchIndexing.add_its(pkg_dict)
SearchIndexing.add_register(pkg_dict)
SearchIndexing.add_api_flag(pkg_dict)
SearchIndexing.add_theme(pkg_dict)
if is_plugin_enabled('dgu_schema'):
SearchIndexing.add_schema(pkg_dict)
SearchIndexing.add_collections(pkg_dict)
return pkg_dict
class ApiPlugin(p.SingletonPlugin):
'''DGU-specific API'''
p.implements(p.IRoutes, inherit=True)
p.implements(p.IActions)
def before_map(self, map):
api_controller = 'ckanext.dgu.controllers.api:DguApiController'
map.connect('/api/util/latest-datasets', controller=api_controller, action='latest_datasets')
map.connect('/api/util/dataset-count', controller=api_controller, action='dataset_count')
map.connect('/api/util/revisions', controller=api_controller, action='revisions')
map.connect('/api/util/latest-unpublished', controller=api_controller, action='latest_unpublished')
map.connect('/api/util/popular-unpublished', controller=api_controller, action='popular_unpublished')
return map
def get_actions(self):
from ckanext.dgu.logic.action.get import publisher_show, suggest_themes
return {
'publisher_show': publisher_show,
'suggest_themes': suggest_themes,
}
class SiteIsDownPlugin(p.SingletonPlugin):
'''"Site is down for maintenance" message shown for all requests - better
than an error while there is maintenance.'''
p.implements(p.IMiddleware, inherit=True)
def make_middleware(self, app, config):
return SiteDownMiddleware(app, config)
class SchemaPlugin(p.SingletonPlugin):
'''Schemas & Code lists'''
p.implements(p.IActions)
p.implements(p.IAuthFunctions)
# IActions
def get_actions(self):
from ckanext.dgu.logic.action.get import schema_list, codelist_list
return {
'schema_list': schema_list,
'codelist_list': codelist_list,
}
# IAuthFunctions
def get_auth_functions(self):
from ckanext.dgu.logic.auth.get import schema_list, codelist_list
return {
'schema_list': schema_list,
'codelist_list': codelist_list,
}
class DguSpatialPlugin(p.SingletonPlugin):
"""
DGU-specific bits to do with spatial that are not in ckanext-spatial
"""
p.implements(p.IDomainObjectModification, inherit=True)
# IDomainObjectModification
def notify(self, entity, operation=None):
from ckan import model
if not isinstance(entity, model.Package):
return
pkg = entity
if not p.toolkit.asbool(pkg.extras.get('UKLP')):
return
log.debug('Notified of UKLP package event: %s %s', pkg.name, operation)
# avoid infinite loop
run_gemini_postprocess = \
self._is_it_sufficient_change_to_run_gemini_postprocess(
entity, operation)
if not run_gemini_postprocess:
return
log.debug('Creating gemini post-process task: %s', pkg.name)
gemini_postprocess_tasks.create_package_task(entity, 'priority')
# similar to ckanext-archiver's _is_it_sufficient_change_to_run_archiver
def _is_it_sufficient_change_to_run_gemini_postprocess(self, package,
operation):
''' Returns True if it is a new dataset or there are resources that
have been added or URL changed in this revision.
'''
from ckan import model
if operation == 'new':
log.debug('New package - will process')
# even if it has no resources, QA needs to show 0 stars against it
return True
elif operation == 'deleted':
log.debug('Deleted package - won\'t process')
return False
# therefore operation=changed
# check to see if resources are added or URL changed
# look for the latest revision
rev_list = package.all_related_revisions
if not rev_list:
log.debug('No sign of previous revisions - will process')
return True
# I am not confident we can rely on the info about the current
# revision, because we are still in the 'before_commit' stage. So
# simply ignore that if it's returned.
if rev_list[0][0].id == model.Session.revision.id:
rev_list = rev_list[1:]
if not rev_list:
log.warn('No sign of previous revisions - will process')
return True
previous_revision = rev_list[0][0]
log.debug('Comparing with revision: %s %s',
previous_revision.timestamp, previous_revision.id)
# get the package as it was at that previous revision
context = {'model': model, 'session': model.Session,
#'user': c.user or c.author,
'ignore_auth': True,
'revision_id': previous_revision.id}
data_dict = {'id': package.id}
try:
old_pkg_dict = p.toolkit.get_action('package_show')(
context, data_dict)
except p.toolkit.NotFound:
log.warn('No sign of previous package - will process anyway')
return True
# have any resources been added?
old_resources = dict((res['id'], res)
for res in old_pkg_dict['resources'])
old_res_ids = set(old_resources.keys())
new_res_ids = set((res.id for res in package.resources))
added_res_ids = new_res_ids - old_res_ids
if added_res_ids:
log.debug('Added resources - will process. res_ids=%r',
added_res_ids)
return True
# have any resource urls changed?
for res in package.resources:
old_res_url = old_resources[res.id]['url']
if old_res_url != res.url:
log.debug('Resource url changed - will process. '
'id=%s pos=%s url="%s"->"%s"',
res.id[:4], res.position, old_res_url, res.url)
return True
log.debug('Resource unchanged. pos=%s id=%s',
res.position, res.id[:4])
log.debug('No new or changed resources - won\'t process')
return False
class DguPublisherFiles(p.SingletonPlugin):
p.implements(p.IRoutes, inherit=True)
def after_map(self, map):
data_ctlr = 'ckanext.dgu.controllers.data:DataController'
map.connect('publisher_files', '/publisher-files{rel_path:.*?}',
controller=data_ctlr, action='publisher_files')
return map