From 37e711cd2fc677650be8b2278bcd2d888ff74bc6 Mon Sep 17 00:00:00 2001 From: David Read Date: Mon, 16 Jan 2012 20:44:20 +0000 Subject: [PATCH 01/46] [defect-1401-mount-non-root][#1401][templates,lib]: Fix links to take note of mount point. js and css not fixed yet. --- ckan/lib/helpers.py | 5 +-- ckan/public/css/style.css | 4 +- ckan/templates/_util.html | 2 +- ckan/templates/group/new_group_form.html | 2 +- ckan/templates/layout_base.html | 44 ++++++++++---------- ckan/templates/package/layout.html | 4 +- ckan/templates/package/new_package_form.html | 2 +- ckan/templates/package/resource_read.html | 20 ++++----- ckan/templates/user/login.html | 8 ++-- doc/theming.rst | 2 +- 10 files changed, 46 insertions(+), 47 deletions(-) diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index e61c5ed5f1d..8f6f7bb8774 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -198,8 +198,7 @@ def linked_user(user, maxlength=0): _name = user.name if model.User.VALID_NAME.match(user.name) else user.id # Absolute URL of default user icon from pylons import config - _site_url = config.get('ckan.site_url', '') - _icon_url_default = _site_url + icon_url("user") + _icon_url_default = icon_url("user") _icon = gravatar(user.email_hash, 16, _icon_url_default)+" " displayname = user.display_name if maxlength and len(user.display_name) > maxlength: @@ -222,7 +221,7 @@ def markdown_extract(text, extract_length=190): return unicode(truncate(plain, length=extract_length, indicator='...', whole_word=True)) def icon_url(name): - return '/images/icons/%s.png' % name + return url_for('/images/icons/%s.png' % name) def icon_html(url, alt=None): return literal('%s ' % (url, alt)) diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 6980e383490..70abf6f4148 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -1,5 +1,5 @@ -@import url('/css/forms.css'); -@import url('/css/pretty_buttons.css'); +@import url('forms.css'); +@import url('pretty_buttons.css'); body.no-sidebar #sidebar { display: none; } body.no-sidebar #content { diff --git a/ckan/templates/_util.html b/ckan/templates/_util.html index 26f1411d0ad..a04268eca70 100644 --- a/ckan/templates/_util.html +++ b/ckan/templates/_util.html @@ -183,7 +183,7 @@ ${'OPEN' if package.isopen() else 'CLOSED'} diff --git a/ckan/templates/group/new_group_form.html b/ckan/templates/group/new_group_form.html index 84a33053b36..2c01d64e996 100644 --- a/ckan/templates/group/new_group_form.html +++ b/ckan/templates/group/new_group_form.html @@ -19,7 +19,7 @@

Errors in form

- ${g.site_url+h.url_for(controller='group', action='index')+'/'}  + ${url(controller='group', action='index')+'/'} 

 

diff --git a/ckan/templates/layout_base.html b/ckan/templates/layout_base.html index 2ca6b2f5135..a096b4ccebc 100644 --- a/ckan/templates/layout_base.html +++ b/ckan/templates/layout_base.html @@ -17,7 +17,7 @@ - + @@ -29,14 +29,14 @@ - - - - + + + + - + ${optional_head()} @@ -134,7 +134,7 @@

About ${g.site_title}

  • ${h.link_to(_('API'), h.url_for(controller='api', action='get_api'))}
  • ${h.link_to(_('API Docs'), 'http://wiki.ckan.net/API')}
  • - Contact Us + Contact Us
  • Privacy Policy @@ -218,25 +218,25 @@

    Meta

    - - - - - - + + + + + + - - - - - + + + + + - - + + - + - - - - - - + + + + + + + diff --git a/ckan/templates/user/login.html b/ckan/templates/user/login.html index 2b6fe7148e2..a24b866b016 100644 --- a/ckan/templates/user/login.html +++ b/ckan/templates/user/login.html @@ -5,10 +5,10 @@ - + - + - - + +
    From df22daad703d4acbc5a90db6e3b290e8212e910a Mon Sep 17 00:00:00 2001 From: Tom Rees Date: Wed, 18 Jan 2012 13:17:47 +0000 Subject: [PATCH 03/46] [#1521][m]: Refactored notes expand/collapse feature to work on groups page. --- ckan/public/css/style.css | 9 +++------ ckan/public/scripts/application.js | 18 ++++++++++++------ ckan/public/scripts/templates.js | 4 ++-- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 6980e383490..f9b434e1699 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -594,9 +594,6 @@ form.simple-form input[type=password] { .group.read .property-list li ul li { margin-left: -2.5em; } -.group.read .notes p { - margin-bottom: 0; -} .group-dataset-list { margin: 2em 0; } @@ -1177,16 +1174,16 @@ body.package.read #sidebar li.widget-container { border-left: 2px solid #eee; background: url('/images/ldquo.png') no-repeat top left #f7f7f7; } -.notes #dataset-notes-toggle a { +.notes #notes-toggle a { cursor: pointer; } -.notes #dataset-notes-toggle a.more:after { +.notes #notes-toggle a.more:after { content: ' »'; font-size: 150%; position: relative; bottom: -1px; } -.notes #dataset-notes-toggle a.less:before { +.notes #notes-toggle a.less:before { content: '« '; font-size: 150%; position: relative; diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index fee666df39f..0af84b7a015 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -23,10 +23,16 @@ CKAN.Utils.setupWelcomeBanner($('.js-welcome-banner')); } + var isGroupView = $('body.group.read').length > 0; + if (isGroupView) { + // Show extract of notes field + CKAN.Utils.setupNotesExtract(); + } + var isDatasetView = $('body.package.read').length > 0; if (isDatasetView) { // Show extract of notes field - CKAN.Utils.setupDatasetViewNotesExtract(); + CKAN.Utils.setupNotesExtract(); } var isResourceView = $('body.package.resource_read').length > 0; @@ -424,17 +430,17 @@ CKAN.Utils = function($, my) { // If notes field is more than 1 paragraph, just show the // first paragraph with a 'Read more' link that will expand // the div if clicked - my.setupDatasetViewNotesExtract = function() { - var notes = $('#dataset div.notes'); + my.setupNotesExtract = function() { + var notes = $('#content div.notes'); if(notes.find('p').length > 1){ var extract = notes.children(':eq(0)'); var remainder = notes.children(':gt(0)'); - notes.html($.tmpl(CKAN.Templates.datasetNotesField)); + notes.html($.tmpl(CKAN.Templates.notesField)); notes.find('#notes-extract').html(extract); notes.find('#notes-remainder').html(remainder); notes.find('#notes-remainder').hide(); - notes.find('#dataset-notes-toggle a').click(function(event){ - notes.find('#dataset-notes-toggle a').toggle(); + notes.find('#notes-toggle a').click(function(event){ + notes.find('#notes-toggle a').toggle(); var remainder = notes.find('#notes-remainder') if ($(event.target).hasClass('more')) { remainder.slideDown(); diff --git a/ckan/public/scripts/templates.js b/ckan/public/scripts/templates.js index ca4600a4322..3142dadaf0b 100644 --- a/ckan/public/scripts/templates.js +++ b/ckan/public/scripts/templates.js @@ -153,11 +153,11 @@ CKAN.Templates.resourceEntry = ' \ \ '; -CKAN.Templates.datasetNotesField = ' \ +CKAN.Templates.notesField = ' \
    \
    \
    \
    \ - \ + \ '; From 088eaaf045cf29980dc0e61a3bf0ae1d0d05a231 Mon Sep 17 00:00:00 2001 From: Tom Rees Date: Wed, 18 Jan 2012 15:15:45 +0000 Subject: [PATCH 04/46] [#1521][m]: Querystring can specify a group when creating new datasets. Linked from group page. --- ckan/templates/group/layout.html | 16 +++++++++++++++- ckan/templates/package/new_package_form.html | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/ckan/templates/group/layout.html b/ckan/templates/group/layout.html index a5204e7af2c..2c50e98e187 100644 --- a/ckan/templates/group/layout.html +++ b/ckan/templates/group/layout.html @@ -9,13 +9,27 @@
    • ${h.subnav_link(c, h.icon('group') + _('View'), controller='group', action='read', id=c.group.name)}
    • +
    • ${h.subnav_link(c, h.icon('page_white_stack') + _('History'), controller='group', action='history', id=c.group.name)}
    • +   |   +
    • ${h.subnav_link(c, h.icon('group_edit') + _('Edit'), controller='group', action='edit', id=c.group.name)}
    • -
    • ${h.subnav_link(c, h.icon('page_white_stack') + _('History'), controller='group', action='history', id=c.group.name)}
    • ${h.subnav_link(c, h.icon('lock') + _('Authorization'), controller='group', action='authz', id=c.group.name)}
    • + ${h.subnav_named_route( c,h.icon('group_edit') + _('Edit'), c.group.type + '_action', action='edit', id=c.group.name )} - ${h.subnav_link(c, h.icon('group_edit') + _('Edit'), controller='group', action='edit', id=c.group.name)}
    • ${h.subnav_named_route(c, h.icon('lock') + _('Authorization'), c.group.type + '_action', controller='group', action='authz', id=c.group.name)} From 95606b51ed7eb37a9f5453105b5f1259fcf5af3d Mon Sep 17 00:00:00 2001 From: Tom Rees Date: Thu, 26 Jan 2012 17:57:17 +0000 Subject: [PATCH 34/46] [#1716][m]: Moved the unsaved changes warning to the bottom of the page to save nav-links from jumping out of the way of the mouse. --- ckan/public/scripts/application.js | 8 +++++++- ckan/templates/js_strings.html | 2 +- ckan/templates/package/new_package_form.html | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 48dc00e1acd..2c417429988 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -492,7 +492,13 @@ CKAN.View.DatasetEditForm = Backbone.View.extend({ var boundToUnload = false; return function() { if (!boundToUnload) { - CKAN.Utils.flashMessage(CKAN.Strings.youHaveUnsavedChanges,'notice'); + var parentDiv = $('
      ').addClass('flash-messages'); + var messageDiv = $('
      ').html(CKAN.Strings.youHaveUnsavedChanges).addClass('notice').hide(); + parentDiv.append(messageDiv); + $('#unsaved-warning').append(parentDiv); + console.log($('#unsaved-warning')); + messageDiv.show(1200); + boundToUnload = true; window.onbeforeunload = function () { return CKAN.Strings.youHaveUnsavedChanges; diff --git a/ckan/templates/js_strings.html b/ckan/templates/js_strings.html index e8b9c8ced14..1258a1105fb 100644 --- a/ckan/templates/js_strings.html +++ b/ckan/templates/js_strings.html @@ -22,7 +22,7 @@ CKAN.Strings.failedToSave = "${_('Failed to save, possibly due to invalid data ')}"; CKAN.Strings.addDataset = "${_('Add Dataset')}"; CKAN.Strings.addGroup = "${_('Add Group')}"; - CKAN.Strings.youHaveUnsavedChanges = "${_('You have unsaved changes. Hit Save Changes at the bottom of the page to submit them.')}"; + CKAN.Strings.youHaveUnsavedChanges = "${_('You have unsaved changes. Hit Save Changes below submit them.')}"; CKAN.Strings.loading = "${_('Loading...')}"; CKAN.Strings.noNameBrackets = "${_('(no name)')}"; CKAN.Strings.deleteThisResourceQuestion = "${_('Delete the resource \'%name%\'?')}" diff --git a/ckan/templates/package/new_package_form.html b/ckan/templates/package/new_package_form.html index 718ee1356f3..bbf2f5f2b7c 100644 --- a/ckan/templates/package/new_package_form.html +++ b/ckan/templates/package/new_package_form.html @@ -200,6 +200,8 @@

      Tags

      +
      + From eda8cbac06dfe750c2b48eb55e57ed25d2dcd3ac Mon Sep 17 00:00:00 2001 From: Tom Rees Date: Thu, 26 Jan 2012 18:03:51 +0000 Subject: [PATCH 35/46] [#1716][xs]: Hotfix to string content. Closes #1716. --- ckan/templates/js_strings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/js_strings.html b/ckan/templates/js_strings.html index 1258a1105fb..55c056bdc96 100644 --- a/ckan/templates/js_strings.html +++ b/ckan/templates/js_strings.html @@ -22,7 +22,7 @@ CKAN.Strings.failedToSave = "${_('Failed to save, possibly due to invalid data ')}"; CKAN.Strings.addDataset = "${_('Add Dataset')}"; CKAN.Strings.addGroup = "${_('Add Group')}"; - CKAN.Strings.youHaveUnsavedChanges = "${_('You have unsaved changes. Hit Save Changes below submit them.')}"; + CKAN.Strings.youHaveUnsavedChanges = "${_("You have unsaved changed. Make sure to click 'Save Changes' below before leaving this page.")}"; CKAN.Strings.loading = "${_('Loading...')}"; CKAN.Strings.noNameBrackets = "${_('(no name)')}"; CKAN.Strings.deleteThisResourceQuestion = "${_('Delete the resource \'%name%\'?')}" From a348b36d1093e91deaa4e27541e6444e8ea81d6b Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 26 Jan 2012 19:07:12 +0100 Subject: [PATCH 36/46] Fix a couple of typos --- ckan/lib/navl/dictization_functions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ckan/lib/navl/dictization_functions.py b/ckan/lib/navl/dictization_functions.py index 30f10afb3a7..88818d2aaac 100644 --- a/ckan/lib/navl/dictization_functions.py +++ b/ckan/lib/navl/dictization_functions.py @@ -60,9 +60,10 @@ def flatten_schema(schema, flattened=None, key=None): return flattened def get_all_key_combinations(data, flattented_schema): - '''compare the schema agaist the given data and get all valid tuples that - match the schema ignoring the last value in the tuple.''' + '''Compare the schema against the given data and get all valid tuples that + match the schema ignoring the last value in the tuple. + ''' schema_prefixes = set([key[:-1] for key in flattented_schema]) combinations = set([()]) @@ -206,7 +207,7 @@ def _remove_blank_keys(schema): return schema def validate(data, schema, context=None): - '''validate an unflattened nested dict agiast a schema''' + '''Validate an unflattened nested dict against a schema.''' context = context or {} From f4d991282b850a2c8fa8ccccdbe5e3d5abda5fcc Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 26 Jan 2012 19:14:59 +0100 Subject: [PATCH 37/46] Fix a typo in default_resource_schema() --- ckan/logic/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index c53dbe74c25..cd408fd4fd8 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -39,7 +39,7 @@ def default_resource_schema(): schema = { 'id': [ignore_empty, unicode], - 'revistion_id': [ignore_missing, unicode], + 'revision_id': [ignore_missing, unicode], 'resource_group_id': [ignore], 'package_id': [ignore], 'url': [ignore_empty, unicode],#, URL(add_http=False)], From 68a15ec3f3d1f4f1e396dd27c8564895f4d05490 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 26 Jan 2012 19:15:41 +0100 Subject: [PATCH 38/46] Fix a typo in a comment --- ckan/migration/versions/034_resource_group_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/migration/versions/034_resource_group_table.py b/ckan/migration/versions/034_resource_group_table.py index cc2e257b30b..e7d6890ab13 100644 --- a/ckan/migration/versions/034_resource_group_table.py +++ b/ckan/migration/versions/034_resource_group_table.py @@ -110,7 +110,7 @@ def upgrade(migrate_engine): # do data transfer # give resource group a hashed version of package uuid # so that we can use the same hash calculation on - # the resource and resource revistion table + # the resource and resource revision table migrate_engine.execute(''' insert into resource_group select From 5616bc2053c8773147acfbf68a48c867656d49e6 Mon Sep 17 00:00:00 2001 From: Tom Rees Date: Thu, 26 Jan 2012 19:15:34 +0000 Subject: [PATCH 39/46] [#1425][m]: Redesigned front-end for dataset deletion. Closes #1425. --- ckan/public/css/style.css | 3 +++ ckan/public/scripts/application.js | 16 +++++++++++++-- ckan/templates/package/edit.html | 1 + ckan/templates/package/new_package_form.html | 21 ++++++++++++-------- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 5b6cf79299c..34a2ec04046 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -963,6 +963,9 @@ ul.dataset-edit-nav li a:hover { .dataset-edit-form .resource-add .fileinfo { margin: 7px 0; } +.dataset-edit-form button.dataset-delete { + vertical-align: top; +} /* ================================ */ diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 2c417429988..d7d3a0825b5 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -80,6 +80,18 @@ el: $el }); view.render(); + + // Set up dataset delete button + var select = $('select.dataset-delete'); + select.attr('disabled','disabled'); + select.css({opacity: 0.3}); + $('button.dataset-delete').click(function(e) { + select.removeAttr('disabled'); + select.fadeTo('fast',1.0); + $(e.target).css({opacity:0}); + $(e.target).attr('disabled','disabled'); + return false; + }); } var isGroupEdit = $('body.group.edit').length > 0; if (isGroupEdit) { @@ -497,7 +509,7 @@ CKAN.View.DatasetEditForm = Backbone.View.extend({ parentDiv.append(messageDiv); $('#unsaved-warning').append(parentDiv); console.log($('#unsaved-warning')); - messageDiv.show(1200); + messageDiv.show(200); boundToUnload = true; window.onbeforeunload = function () { @@ -507,7 +519,7 @@ CKAN.View.DatasetEditForm = Backbone.View.extend({ } }(); - $form.find('input').live('change', function(e) { + $form.find('input,select').live('change', function(e) { $target = $(e.target); // Entering text in the 'add' box does not represent a change if ($target.closest('.resource-add').length==0) { diff --git a/ckan/templates/package/edit.html b/ckan/templates/package/edit.html index 1028975da88..6bd9a39735c 100644 --- a/ckan/templates/package/edit.html +++ b/ckan/templates/package/edit.html @@ -23,6 +23,7 @@
    • Resources
    • Groups & Tags
    • Extras
    • +
    • Delete
  • diff --git a/ckan/templates/package/new_package_form.html b/ckan/templates/package/new_package_form.html index bbf2f5f2b7c..788e590162c 100644 --- a/ckan/templates/package/new_package_form.html +++ b/ckan/templates/package/new_package_form.html @@ -165,14 +165,6 @@

    Tags

    A number representing the version (if applicable)
    e.g. 1.2.0
    -
    -
    - -
    - @@ -199,6 +191,19 @@

    Tags

    +
    +
    +
    Delete
    +
    +

    Do you really want to change the state of this dataset?   

    + This dataset is   + +
    +
    +
    From f3a4c3de70da08d396da4e65a5d286b253be0e0b Mon Sep 17 00:00:00 2001 From: Tom Rees Date: Thu, 26 Jan 2012 19:15:34 +0000 Subject: [PATCH 40/46] [#1425][m]: Redesigned front-end for dataset deletion. Closes #1425. --- ckan/public/css/style.css | 3 +++ ckan/public/scripts/application.js | 16 +++++++++++++-- ckan/templates/package/edit.html | 1 + ckan/templates/package/new_package_form.html | 21 ++++++++++++-------- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 5b6cf79299c..34a2ec04046 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -963,6 +963,9 @@ ul.dataset-edit-nav li a:hover { .dataset-edit-form .resource-add .fileinfo { margin: 7px 0; } +.dataset-edit-form button.dataset-delete { + vertical-align: top; +} /* ================================ */ diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 2c417429988..d7d3a0825b5 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -80,6 +80,18 @@ el: $el }); view.render(); + + // Set up dataset delete button + var select = $('select.dataset-delete'); + select.attr('disabled','disabled'); + select.css({opacity: 0.3}); + $('button.dataset-delete').click(function(e) { + select.removeAttr('disabled'); + select.fadeTo('fast',1.0); + $(e.target).css({opacity:0}); + $(e.target).attr('disabled','disabled'); + return false; + }); } var isGroupEdit = $('body.group.edit').length > 0; if (isGroupEdit) { @@ -497,7 +509,7 @@ CKAN.View.DatasetEditForm = Backbone.View.extend({ parentDiv.append(messageDiv); $('#unsaved-warning').append(parentDiv); console.log($('#unsaved-warning')); - messageDiv.show(1200); + messageDiv.show(200); boundToUnload = true; window.onbeforeunload = function () { @@ -507,7 +519,7 @@ CKAN.View.DatasetEditForm = Backbone.View.extend({ } }(); - $form.find('input').live('change', function(e) { + $form.find('input,select').live('change', function(e) { $target = $(e.target); // Entering text in the 'add' box does not represent a change if ($target.closest('.resource-add').length==0) { diff --git a/ckan/templates/package/edit.html b/ckan/templates/package/edit.html index 1028975da88..6bd9a39735c 100644 --- a/ckan/templates/package/edit.html +++ b/ckan/templates/package/edit.html @@ -23,6 +23,7 @@
  • Resources
  • Groups & Tags
  • Extras
  • +
  • Delete
  • diff --git a/ckan/templates/package/new_package_form.html b/ckan/templates/package/new_package_form.html index bbf2f5f2b7c..788e590162c 100644 --- a/ckan/templates/package/new_package_form.html +++ b/ckan/templates/package/new_package_form.html @@ -165,14 +165,6 @@

    Tags

    A number representing the version (if applicable)
    e.g. 1.2.0
    -
    -
    - -
    - @@ -199,6 +191,19 @@

    Tags

    +
    +
    +
    Delete
    +
    +

    Do you really want to change the state of this dataset?   

    + This dataset is   + +
    +
    +
    From 9cee66a878b0b3fb7733730b0ebc942217bc94ba Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 22:45:58 +0000 Subject: [PATCH 41/46] [#1602,js/vendor][s]: add jquery.mustache. --- .../vendor/jquery.mustache/jquery.mustache.js | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100755 ckan/public/scripts/vendor/jquery.mustache/jquery.mustache.js diff --git a/ckan/public/scripts/vendor/jquery.mustache/jquery.mustache.js b/ckan/public/scripts/vendor/jquery.mustache/jquery.mustache.js new file mode 100755 index 00000000000..5aa67def818 --- /dev/null +++ b/ckan/public/scripts/vendor/jquery.mustache/jquery.mustache.js @@ -0,0 +1,346 @@ +/* +Shameless port of a shameless port +@defunkt => @janl => @aq + +See http://github.com/defunkt/mustache for more info. +*/ + +;(function($) { + +/* + mustache.js — Logic-less templates in JavaScript + + See http://mustache.github.com/ for more info. +*/ + +var Mustache = function() { + var Renderer = function() {}; + + Renderer.prototype = { + otag: "{{", + ctag: "}}", + pragmas: {}, + buffer: [], + pragmas_implemented: { + "IMPLICIT-ITERATOR": true + }, + context: {}, + + render: function(template, context, partials, in_recursion) { + // reset buffer & set context + if(!in_recursion) { + this.context = context; + this.buffer = []; // TODO: make this non-lazy + } + + // fail fast + if(!this.includes("", template)) { + if(in_recursion) { + return template; + } else { + this.send(template); + return; + } + } + + template = this.render_pragmas(template); + var html = this.render_section(template, context, partials); + if(in_recursion) { + return this.render_tags(html, context, partials, in_recursion); + } + + this.render_tags(html, context, partials, in_recursion); + }, + + /* + Sends parsed lines + */ + send: function(line) { + if(line != "") { + this.buffer.push(line); + } + }, + + /* + Looks for %PRAGMAS + */ + render_pragmas: function(template) { + // no pragmas + if(!this.includes("%", template)) { + return template; + } + + var that = this; + var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + + this.ctag); + return template.replace(regex, function(match, pragma, options) { + if(!that.pragmas_implemented[pragma]) { + throw({message: + "This implementation of mustache doesn't understand the '" + + pragma + "' pragma"}); + } + that.pragmas[pragma] = {}; + if(options) { + var opts = options.split("="); + that.pragmas[pragma][opts[0]] = opts[1]; + } + return ""; + // ignore unknown pragmas silently + }); + }, + + /* + Tries to find a partial in the curent scope and render it + */ + render_partial: function(name, context, partials) { + name = this.trim(name); + if(!partials || partials[name] === undefined) { + throw({message: "unknown_partial '" + name + "'"}); + } + if(typeof(context[name]) != "object") { + return this.render(partials[name], context, partials, true); + } + return this.render(partials[name], context[name], partials, true); + }, + + /* + Renders inverted (^) and normal (#) sections + */ + render_section: function(template, context, partials) { + if(!this.includes("#", template) && !this.includes("^", template)) { + return template; + } + + var that = this; + // CSW - Added "+?" so it finds the tighest bound, not the widest + var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + + "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + + "\\s*", "mg"); + + // for each {{#foo}}{{/foo}} section do... + return template.replace(regex, function(match, type, name, content) { + var value = that.find(name, context); + if(type == "^") { // inverted section + if(!value || that.is_array(value) && value.length === 0) { + // false or empty list, render it + return that.render(content, context, partials, true); + } else { + return ""; + } + } else if(type == "#") { // normal section + if(that.is_array(value)) { // Enumerable, Let's loop! + return that.map(value, function(row) { + return that.render(content, that.create_context(row), + partials, true); + }).join(""); + } else if(that.is_object(value)) { // Object, Use it as subcontext! + return that.render(content, that.create_context(value), + partials, true); + } else if(typeof value === "function") { + // higher order section + return value.call(context, content, function(text) { + return that.render(text, context, partials, true); + }); + } else if(value) { // boolean section + return that.render(content, context, partials, true); + } else { + return ""; + } + } + }); + }, + + /* + Replace {{foo}} and friends with values from our view + */ + render_tags: function(template, context, partials, in_recursion) { + // tit for tat + var that = this; + + var new_regex = function() { + return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + + that.ctag + "+", "g"); + }; + + var regex = new_regex(); + var tag_replace_callback = function(match, operator, name) { + switch(operator) { + case "!": // ignore comments + return ""; + case "=": // set new delimiters, rebuild the replace regexp + that.set_delimiters(name); + regex = new_regex(); + return ""; + case ">": // render partial + return that.render_partial(name, context, partials); + case "{": // the triple mustache is unescaped + return that.find(name, context); + default: // escape the value + return that.escape(that.find(name, context)); + } + }; + var lines = template.split("\n"); + for(var i = 0; i < lines.length; i++) { + lines[i] = lines[i].replace(regex, tag_replace_callback, this); + if(!in_recursion) { + this.send(lines[i]); + } + } + + if(in_recursion) { + return lines.join("\n"); + } + }, + + set_delimiters: function(delimiters) { + var dels = delimiters.split(" "); + this.otag = this.escape_regex(dels[0]); + this.ctag = this.escape_regex(dels[1]); + }, + + escape_regex: function(text) { + // thank you Simon Willison + if(!arguments.callee.sRE) { + var specials = [ + '/', '.', '*', '+', '?', '|', + '(', ')', '[', ']', '{', '}', '\\' + ]; + arguments.callee.sRE = new RegExp( + '(\\' + specials.join('|\\') + ')', 'g' + ); + } + return text.replace(arguments.callee.sRE, '\\$1'); + }, + + /* + find `name` in current `context`. That is find me a value + from the view object + */ + find: function(name, context) { + name = this.trim(name); + + // Checks whether a value is thruthy or false or 0 + function is_kinda_truthy(bool) { + return bool === false || bool === 0 || bool; + } + + var value; + if(is_kinda_truthy(context[name])) { + value = context[name]; + } else if(is_kinda_truthy(this.context[name])) { + value = this.context[name]; + } + + if(typeof value === "function") { + return value.apply(context); + } + if(value !== undefined) { + return value; + } + // silently ignore unkown variables + return ""; + }, + + // Utility methods + + /* includes tag */ + includes: function(needle, haystack) { + return haystack.indexOf(this.otag + needle) != -1; + }, + + /* + Does away with nasty characters + */ + escape: function(s) { + s = String(s === null ? "" : s); + return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { + switch(s) { + case "&": return "&"; + case "\\": return "\\\\"; + case '"': return '\"'; + case "<": return "<"; + case ">": return ">"; + default: return s; + } + }); + }, + + // by @langalex, support for arrays of strings + create_context: function(_context) { + if(this.is_object(_context)) { + return _context; + } else { + var iterator = "."; + if(this.pragmas["IMPLICIT-ITERATOR"]) { + iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; + } + var ctx = {}; + ctx[iterator] = _context; + return ctx; + } + }, + + is_object: function(a) { + return a && typeof a == "object"; + }, + + is_array: function(a) { + return Object.prototype.toString.call(a) === '[object Array]'; + }, + + /* + Gets rid of leading and trailing whitespace + */ + trim: function(s) { + return s.replace(/^\s*|\s*$/g, ""); + }, + + /* + Why, why, why? Because IE. Cry, cry cry. + */ + map: function(array, fn) { + if (typeof array.map == "function") { + return array.map(fn); + } else { + var r = []; + var l = array.length; + for(var i = 0; i < l; i++) { + r.push(fn(array[i])); + } + return r; + } + } + }; + + return({ + name: "mustache.js", + version: "0.3.1-dev", + + /* + Turns a template and view into HTML + */ + to_html: function(template, view, partials, send_fun) { + var renderer = new Renderer(); + if(send_fun) { + renderer.send = send_fun; + } + renderer.render(template, view, partials); + if(!send_fun) { + return renderer.buffer.join("\n"); + } + }, + escape : function(text) { + return new Renderer().escape(text); + } + }); +}(); + + $.mustache = function(template, view, partials) { + return Mustache.to_html(template, view, partials); + }; + + $.mustache.escape = function(text) { + return Mustache.escape(text); + }; + +})(jQuery); From 92504e7f9b8ac74d29f4a31b14d99b273deba68c Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 23:11:49 +0000 Subject: [PATCH 42/46] [#1602,js/vendor][m]: add in recline data explorer library. * Recline is (non-minified) consolidated as of https://github.com/okfn/recline/commit/8fa93dcbe9d076774bd04b14cc128ab634f6e800. --- .../vendor/recline/css/data-explorer.css | 526 +++++ .../scripts/vendor/recline/css/graph-flot.css | 50 + .../vendor/recline/css/images/edit-map.png | Bin 0 -> 1569 bytes .../recline/css/images/menu-dropdown.png | Bin 0 -> 1123 bytes ckan/public/scripts/vendor/recline/recline.js | 1836 +++++++++++++++++ 5 files changed, 2412 insertions(+) create mode 100644 ckan/public/scripts/vendor/recline/css/data-explorer.css create mode 100644 ckan/public/scripts/vendor/recline/css/graph-flot.css create mode 100755 ckan/public/scripts/vendor/recline/css/images/edit-map.png create mode 100755 ckan/public/scripts/vendor/recline/css/images/menu-dropdown.png create mode 100644 ckan/public/scripts/vendor/recline/recline.js diff --git a/ckan/public/scripts/vendor/recline/css/data-explorer.css b/ckan/public/scripts/vendor/recline/css/data-explorer.css new file mode 100644 index 00000000000..2705d14a63c --- /dev/null +++ b/ckan/public/scripts/vendor/recline/css/data-explorer.css @@ -0,0 +1,526 @@ +.data-explorer .header .navigation, +.data-explorer .header .navigation li, +.data-explorer .header .pagination, +.data-explorer .header .pagination form +{ + display: inline; +} + +.data-explorer .header .navigation { + float: left; + margin-left: 0; + padding-left: 0; +} + +.header .pagination { + float: right; + margin: 4px; +} + +.header .pagination label { + float: none; +} + +.header .pagination input { + width: 30px; +} + +.doc-count { + font-weight: bold; + font-size: 120%; +} + +.data-view-container { + display: block; + clear: both; +} + +/* bootstrap btn */ +.btn { + cursor: pointer; + display: inline-block; + background-color: #e6e6e6; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); + background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + padding: 5px 14px 6px; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + color: #333; + font-size: 13px; + line-height: normal; + border: 1px solid #ccc; + border-bottom-color: #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -webkit-transition: 0.1s linear all; + -moz-transition: 0.1s linear all; + -ms-transition: 0.1s linear all; + -o-transition: 0.1s linear all; + transition: 0.1s linear all; +} +.btn:hover { + background-position: 0 -15px; + color: #333; + text-decoration: none; +} +.btn:focus { + outline: 1px dotted #666; +} + +/* twitter btn.disabled but for button link that is active. used in navigation */ +.active .btn { + cursor: default; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + filter: alpha(opacity=65); + -khtml-opacity: 0.65; + -moz-opacity: 0.65; + opacity: 0.65; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + + +/********************************************************** + * Notifications + *********************************************************/ + +.notification-container { + width: 400px; + left: 520px; + display: none; + position: fixed; + top: 0; + z-index: 100; + text-align: center; +} + +.notification { + display: inline-block; + margin: 0 auto; + padding: 5px 8px 4px; + font-size: 1.3em; + text-align: left; + font-weight: bold; + background: #fe8; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; +} + +.notification-action { + padding-left: 10px; +} + +.notification-loader { + padding: 0 3px 0 0; + opacity: 0.3; +} + + +/********************************************************** + * Data Table + *********************************************************/ + +.data-table { + border: 1px solid #ccc; + font-size: 12px; +} + +.data-table td, .data-table th { + border-left: 1px solid #ccc; + padding: 3px 4px; +} + +.data-table tr td:first-child, .data-table tr th:first-child { + width: 20px; +} + +/********************************************************** + * Data Table Menus + *********************************************************/ + +a.column-header-menu { + float: right; + display: block; + margin: 0 4px 0 0; + width: 17px; + height: 19px; + background-image: url(images/menu-dropdown.png); + background-repeat: no-repeat; +} + +a.row-header-menu:hover { + background-position: -17px 0px; + text-decoration: none; +} + +a.row-header-menu { + float: left; + display: block; + margin: -2px 0 -4px 0; + width: 17px; + height: 18px; + background-image: url(images/menu-dropdown.png); + background-repeat: no-repeat; +} + +a.column-header-menu:hover { + background-position: -17px 0px; + text-decoration: none; +} + +.column-header-recon-stats-bar { + margin-top: 10px; + height: 4px; + background: #ddd; + border: 1px solid #ccc; + position: relative; + width: 100%; +} + +.column-header-recon-stats-matched { + position: absolute; + height: 100%; + background: #282; +} + +.column-header-recon-stats-blanks { + position: absolute; + height: 100%; + background: #3d3; +} + +div.data-table-cell-content { + line-height: 1.2; + color: #222; + position: relative; +} + +div.data-table-cell-content-numeric { + text-align: right; +} + +a.data-table-cell-edit { + position: absolute; + top: 0; + right: 0; + display: block; + width: 25px; + height: 16px; + text-decoration: none; + background-image: url(images/edit-map.png); + background-repeat: no-repeat; + visibility: hidden; +} + +a.data-table-cell-edit:hover { + background-position: -25px 0px; +} + +.data-table td:hover .data-table-cell-edit { + visibility: visible; +} + +div.data-table-cell-content-numeric > a.data-table-cell-edit { + left: 0px; + right: auto; +} + +.data-table-value-nonstring { + color: #282; +} + +.data-table-error { + color: red; +} + +.data-table-menu-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +ul.data-table-menu { + display: none; + outline-style: none; + background: white; + color: black; + font-size: 12px; + height: auto; + list-style: none; + overflow: hidden; + position: absolute; + text-align: left; + width: 120px; + z-index: 666; + border: 1px solid #CCC; + border-right: 1px solid #666; + border-bottom: 1px solid #666; + margin: 0; padding: 0; } + ul.data-table-menu * { + margin: 0; + padding: 0; } + ul.data-table-menu a { + line-height: 14px; + color: black; + display: block; + padding: 5px 7px; + text-decoration: none; } + ul.data-table-menu li { + height: 24px; } + ul.data-table-menu li:hover { + background-color: #DBE8F8 } + +/* TODO: not sure the rest of this is needed */ +.data-table-cell-editor, .data-table-topic-popup { + overflow: auto; + border: 1px solid #bcf; + background: #e3e9ff; + padding: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.data-table-topic-popup-header { + padding: 0 0 5px; +} + +.data-table-cell-editor-editor { + overflow: hidden; + display: block; + width: 98%; + height: 3em; + font-family: monospace; + margin: 3px 0; +} + +.data-table-cell-copypaste-editor { + overflow: hidden; + display: block; + width: 98%; + height: 10em; + font-family: monospace; + margin: 3px 0; +} + +.data-table-cell-editor-action { + float: left; + vertical-align: bottom; + text-align: center; +} + +.data-table-cell-editor-key { + font-size: 0.8em; + color: #999; +} + +ul.sorting-dialog-blank-error-positions { + margin: 0; + padding: 5px; + height: 10em; + border: 1px solid #ccc; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +ul.sorting-dialog-blank-error-positions > li { + display: block; + border: 1px solid #ccc; + background: #eee; + padding: 5px; + margin: 2px; + cursor: move; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + + +/********************************************************** + * Dialogs + *********************************************************/ + +.dialog-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #666; + opacity: 0.5; +} + +.dialog { + position: fixed; + left: 0; + width: 100%; + text-align: center; +} + +.dialog-frame { + margin: 0 auto; + text-align: left; + background: white; + border: 1px solid #3a5774; +} + +.dialog-border { + border: 4px solid #c1d9ff; +} + +.dialog-header { + background: #e0edfe; + padding: 10px; + font-weight: bold; + font-size: 1.6em; + color: #000; + cursor: move; +} + +.dialog-body { + overflow: auto; + font-size: 1.3em; + padding: 15px; +} + +.dialog-instruction { + padding: 0 0 7px; +} + +.dialog-footer { + font-size: 1.3em; + background: #eee; + padding: 10px; +} + +.dialog-busy { + width: 400px; + border: none; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +/********************************************************** + * Transform Dialog + *********************************************************/ + +#expression-preview-tabs .ui-tabs-nav li a { + padding: 0.15em 1em; +} + +textarea.expression-preview-code { + font-family: monospace; + height: 5em; + vertical-align: top; +} + +.expression-preview-parsing-status { + color: #999; +} + +.expression-preview-parsing-status.error { + color: red; +} + +#expression-preview-tabs-preview, +#expression-preview-tabs-help, +#expression-preview-tabs-history, +#expression-preview-tabs-starred { + padding: 5px; + overflow: hidden; +} + +#expression-preview-tabs-preview > div, +#expression-preview-tabs-help > div, +#expression-preview-tabs-history > div, +#expression-preview-tabs-starred { + height: 200px; + overflow: auto; +} + +#expression-preview-tabs-preview td, #expression-preview-tabs-preview th, +#expression-preview-tabs-help td, #expression-preview-tabs-help th, +#expression-preview-tabs-history td, #expression-preview-tabs-history th, +#expression-preview-tabs-starred td, #expression-preview-tabs-starred th { + padding: 5px; +} + +.expression-preview-table-wrapper { + padding: 7px; +} + +.expression-preview-container td { + padding: 2px 5px; + border-top: 1px solid #ccc; +} + +td.expression-preview-heading { + border-top: none; + background: #ddd; + font-weight: bold; +} + +td.expression-preview-value { + max-width: 250px !important; + overflow-x: hidden; +} + +.expression-preview-special-value { + color: #aaa; +} + +.expression-preview-help-container h3 { + margin-top: 15px; + margin-bottom: 7px; + border-bottom: 1px solid #999; +} + +.expression-preview-doc-item-title { + font-weight: bold; + text-align: right; +} + +.expression-preview-doc-item-params { +} + +.expression-preview-doc-item-returns { +} + +.expression-preview-doc-item-desc { + color: #666; +} + + +/********************************************************** + * Read-only mode + *********************************************************/ + +.read-only .data-table tr td:first-child, +.read-only .data-table tr th:first-child +{ + display: none; +} + +.read-only .column-header-menu, +.read-only .row-header-menu, +.read-only a.data-table-cell-edit +{ + display: none; +} + diff --git a/ckan/public/scripts/vendor/recline/css/graph-flot.css b/ckan/public/scripts/vendor/recline/css/graph-flot.css new file mode 100644 index 00000000000..d50f11e1f37 --- /dev/null +++ b/ckan/public/scripts/vendor/recline/css/graph-flot.css @@ -0,0 +1,50 @@ +.data-graph-container .graph { + height: 500px; + margin-right: 200px; +} + +.data-graph-container .legend table { + width: auto; + margin-bottom: 0; +} + +.data-graph-container .legend td { + padding: 5px; + line-height: 13px; +} + +/********************************************************** + * Editor + *********************************************************/ + +.data-graph-container .editor { + float: right; + width: 200px; + padding-left: 0px; +} + +.data-graph-container .editor-info { + padding-left: 4px; +} + +.data-graph-container .editor-info { + cursor: pointer; +} + +.data-graph-container .editor form { + padding-left: 4px; +} + +.data-graph-container .editor select { + width: 100%; +} + +.data-graph-container .editor-info { + border-bottom: 1px solid #ddd; + margin-bottom: 10px; +} + +.data-graph-container .editor-hide-info p { + display: none; +} + diff --git a/ckan/public/scripts/vendor/recline/css/images/edit-map.png b/ckan/public/scripts/vendor/recline/css/images/edit-map.png new file mode 100755 index 0000000000000000000000000000000000000000..dea0ed1ef4e1a8d301f58c9a24ce7ed3ea297bb6 GIT binary patch literal 1569 zcmeAS@N?(olHy`uVBq!ia0vp^MnEjU!3HElqIG(Jlw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6H#24v4 zq}24xJX@vryZ0+8WTx0Eg`4^s_!c;)W@LI)6{QAO`Gq7`WhYyvDB0U7*i={n4aiL` zNmQuF&B-gas<2f8n`;GRgM{^!6u?SKvTcwn`Gtf;oFf&jvGt@IQ zHZeCh*HJJsFf`CNFw!?P(ls=ndS0-B(gnVDi`Y3yQQ>}q7{VqjotXz1c*X=!fZ;%w&ZVrt;# zXl&>R)9aF-T$-DjR|3v4~Pj*wm=R%;iu*SQ+p9GSx?XmOJ`tUn(FD|7*cU7 z>CgZF_J<1?bQF?q9W(#tu!_6m?23$UnhJWSKZ#p%JhBkKzi_5(b#*A;CI{9qhK(#! znNRX$ty8gmlhDL9A%)RkiX=~{rQwY?YkGL1xMWUvsTOSDWJ=HgMh9oohDyts5;ZzM z4BjLxH1RN#{e9xY%iHz_w+}Y3zmQaG%jT~OFumcOZkJr~@0lKpccGqNlVi|~sSoe0&FAs$jx(tF_mAJT=IFlf8dXZNtP*^2 ze-0gB=&4jT)!PuUYP-SVH*50~y<%ogF#Qqv(Rk0lAM7%x7Njt7CC)f}h~K z$Bs$o@BK~u!DJ%cl4!`n`qbwI->;wCLd*+h1WZe6KRtK%-`1hW%Y2!BWZ4a&gi$?sGPuetgw8Sf*;h z?XxWX;v}t|4V;!PjvE~l9gaM%CaJb>8OCt&WB73QrJ#wOUN}&b*PE)d9Y`Z&QUg>?-MS)IMAw}aDM0P zPls%~w8b1x21#$_aAQn0p4VhtQoc*%#>EX=_`LkK%l~IcubAOpcvMLva*^mko}LA2 zTpQAsM_RK+oHuaFE}M0eaaDSP;io3IvONb5F54#CW6*zu@uRF(bOC#o9y2f<$>^<( gS8GdTVqoH7aNjQEEY|+vJ*d$3boFyt=akR{0GM<+9{>OV literal 0 HcmV?d00001 diff --git a/ckan/public/scripts/vendor/recline/css/images/menu-dropdown.png b/ckan/public/scripts/vendor/recline/css/images/menu-dropdown.png new file mode 100755 index 0000000000000000000000000000000000000000..c733fef7f4b2c5249752673204f7c604a453e5dd GIT binary patch literal 1123 zcmaJ=T}TvB7#&i`EEOw??4fB4GHB;--Q9L_6Wwuh%h|TwiL9r_nd|PL^W)659epYC zA&jWBf?k4pi--y`kqWHd0v}3yDkYM%AT%_DQpE0WCqb&W7-G{cN!0jx$bngAZvq!dU1N$F^N1*$MC zk5I)%*eEn`vZ0ZZiy};*|zO;1c)$qdPQ_dV_1TCw?>6c=0j zo^83cX8Y0W?dym7FN~9Sd#+qfC5nf4c649rK0MvCS=g4m?4%BTE@8d9HjNaJZPT@F zrIjzbVp2iHN$UK={O^y()*WF7-t71^_vB{uL$kPh@#EmdGh^g)?B3(G&r+Qy_9Hu3 Mv@XWquRV3{7e{)Hj{pDw literal 0 HcmV?d00001 diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js new file mode 100644 index 00000000000..ca0e9a28368 --- /dev/null +++ b/ckan/public/scripts/vendor/recline/recline.js @@ -0,0 +1,1836 @@ +// importScripts('lib/underscore.js'); + +onmessage = function(message) { + + function parseCSV(rawCSV) { + var patterns = new RegExp(( + // Delimiters. + "(\\,|\\r?\\n|\\r|^)" + + // Quoted fields. + "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + + // Standard fields. + "([^\"\\,\\r\\n]*))" + ), "gi"); + + var rows = [[]], matches = null; + + while (matches = patterns.exec(rawCSV)) { + var delimiter = matches[1]; + + if (delimiter.length && (delimiter !== ",")) rows.push([]); + + if (matches[2]) { + var value = matches[2].replace(new RegExp("\"\"", "g"), "\""); + } else { + var value = matches[3]; + } + rows[rows.length - 1].push(value); + } + + if(_.isEqual(rows[rows.length -1], [""])) rows.pop(); + + var docs = []; + var headers = _.first(rows); + _.each(_.rest(rows), function(row, rowIDX) { + var doc = {}; + _.each(row, function(cell, idx) { + doc[headers[idx]] = cell; + }) + docs.push(doc); + }) + + return docs; + } + + var docs = parseCSV(message.data.data); + + var req = new XMLHttpRequest(); + + req.onprogress = req.upload.onprogress = function(e) { + if(e.lengthComputable) postMessage({ percent: (e.loaded / e.total) * 100 }); + }; + + req.onreadystatechange = function() { if (req.readyState == 4) postMessage({done: true, response: req.responseText}) }; + req.open('POST', message.data.url); + req.setRequestHeader('Content-Type', 'application/json'); + req.send(JSON.stringify({docs: docs})); +}; +// adapted from https://github.com/harthur/costco. heather rules + +var costco = function() { + + function evalFunction(funcString) { + try { + eval("var editFunc = " + funcString); + } catch(e) { + return {errorMessage: e+""}; + } + return editFunc; + } + + function previewTransform(docs, editFunc, currentColumn) { + var preview = []; + var updated = mapDocs($.extend(true, {}, docs), editFunc); + for (var i = 0; i < updated.docs.length; i++) { + var before = docs[i] + , after = updated.docs[i] + ; + if (!after) after = {}; + if (currentColumn) { + preview.push({before: JSON.stringify(before[currentColumn]), after: JSON.stringify(after[currentColumn])}); + } else { + preview.push({before: JSON.stringify(before), after: JSON.stringify(after)}); + } + } + return preview; + } + + function mapDocs(docs, editFunc) { + var edited = [] + , deleted = [] + , failed = [] + ; + + var updatedDocs = _.map(docs, function(doc) { + try { + var updated = editFunc(_.clone(doc)); + } catch(e) { + failed.push(doc); + return; + } + if(updated === null) { + updated = {_deleted: true}; + edited.push(updated); + deleted.push(doc); + } + else if(updated && !_.isEqual(updated, doc)) { + edited.push(updated); + } + return updated; + }); + + return { + edited: edited, + docs: updatedDocs, + deleted: deleted, + failed: failed + }; + } + + function updateDocs(editFunc) { + var dfd = $.Deferred(); + util.notify("Download entire database into Recline. This could take a while...", {persist: true, loader: true}); + couch.request({url: app.baseURL + "api/json"}).then(function(docs) { + util.notify("Updating " + docs.docs.length + " documents. This could take a while...", {persist: true, loader: true}); + var toUpdate = costco.mapDocs(docs.docs, editFunc).edited; + costco.uploadDocs(toUpdate).then( + function(updatedDocs) { + util.notify(updatedDocs.length + " documents updated successfully"); + recline.initializeTable(app.offset); + dfd.resolve(updatedDocs); + }, + function(err) { + util.notify("Errorz! " + err); + dfd.reject(err); + } + ); + }); + return dfd.promise(); + } + + function updateDoc(doc) { + return couch.request({type: "PUT", url: app.baseURL + "api/" + doc._id, data: JSON.stringify(doc)}) + } + + function uploadDocs(docs) { + var dfd = $.Deferred(); + if(!docs.length) dfd.resolve("Failed: No docs specified"); + couch.request({url: app.baseURL + "api/_bulk_docs", type: "POST", data: JSON.stringify({docs: docs})}) + .then( + function(resp) {ensureCommit().then(function() { + var error = couch.responseError(resp); + if (error) { + dfd.reject(error); + } else { + dfd.resolve(resp); + } + })}, + function(err) { dfd.reject(err.responseText) } + ); + return dfd.promise(); + } + + function ensureCommit() { + return couch.request({url: app.baseURL + "api/_ensure_full_commit", type:'POST', data: "''"}); + } + + function deleteColumn(name) { + var deleteFunc = function(doc) { + delete doc[name]; + return doc; + } + return updateDocs(deleteFunc); + } + + function uploadCSV() { + var file = $('#file')[0].files[0]; + if (file) { + var reader = new FileReader(); + reader.readAsText(file); + reader.onload = function(event) { + var payload = { + url: window.location.href + "/api/_bulk_docs", // todo more robust url composition + data: event.target.result + }; + var worker = new Worker('script/costco-csv-worker.js'); + worker.onmessage = function(event) { + var message = event.data; + if (message.done) { + var error = couch.responseError(JSON.parse(message.response)) + console.log('e',error) + if (error) { + app.emitter.emit(error, 'error'); + } else { + util.notify("Data uploaded successfully!"); + recline.initializeTable(app.offset); + } + util.hide('dialog'); + } else if (message.percent) { + if (message.percent === 100) { + util.notify("Waiting for CouchDB...", {persist: true, loader: true}) + } else { + util.notify("Uploading... " + message.percent + "%"); + } + } else { + util.notify(JSON.stringify(message)); + } + }; + worker.postMessage(payload); + }; + } else { + util.notify('File not selected. Please try again'); + } + }; + + return { + evalFunction: evalFunction, + previewTransform: previewTransform, + mapDocs: mapDocs, + updateDocs: updateDocs, + updateDoc: updateDoc, + uploadDocs: uploadDocs, + deleteColumn: deleteColumn, + ensureCommit: ensureCommit, + uploadCSV: uploadCSV + }; +}(); +this.recline = this.recline || {}; + +// Models module following classic module pattern +recline.Model = function($) { + +var my = {}; + +// A Dataset model. +// +// Other than standard list of Backbone attributes it has two important attributes: +// +// * currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows) +// * docCount: total number of documents in this dataset (obtained on a fetch for this Dataset) +my.Dataset = Backbone.Model.extend({ + __type__: 'Dataset', + initialize: function() { + this.currentDocuments = new my.DocumentList(); + this.docCount = null; + }, + + // AJAX method with promise API to get rows (documents) from the backend. + // + // Resulting DocumentList are used to reset this.currentDocuments and are + // also returned. + // + // :param numRows: passed onto backend getDocuments. + // :param start: passed onto backend getDocuments. + // + // this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here. + // This also illustrates the limitations of separating the Dataset and the Backend + getDocuments: function(numRows, start) { + var self = this; + var dfd = $.Deferred(); + this.backend.getDocuments(this.id, numRows, start).then(function(rows) { + var docs = _.map(rows, function(row) { + return new my.Document(row); + }); + self.currentDocuments.reset(docs); + dfd.resolve(self.currentDocuments); + }); + return dfd.promise(); + }, + + toTemplateJSON: function() { + var data = this.toJSON(); + data.docCount = this.docCount; + return data; + } +}); + +my.Document = Backbone.Model.extend({ + __type__: 'Document' +}); + +my.DocumentList = Backbone.Collection.extend({ + __type__: 'DocumentList', + // webStore: new WebStore(this.url), + model: my.Document +}); + +// Backends section +// ================ + +my.setBackend = function(backend) { + Backbone.sync = backend.sync; +}; + +// Backend which just caches in memory +// +// Does not need to be a backbone model but provides some conveniences +my.BackendMemory = Backbone.Model.extend({ + // Initialize a Backend with a local in-memory dataset. + // + // NB: We can handle one and only one dataset at a time. + // + // :param dataset: the data for a dataset on which operations will be + // performed. Its form should be a hash with metadata and data + // attributes. + // + // - metadata: hash of key/value attributes of any kind (but usually with title attribute) + // - data: hash with 2 keys: + // - headers: list of header names/labels + // - rows: list of hashes, each hash being one row. A row *must* have an id attribute which is unique. + // + // Example of data: + // + // { + // headers: ['x', 'y', 'z'] + // , rows: [ + // {id: 0, x: 1, y: 2, z: 3} + // , {id: 1, x: 2, y: 4, z: 6} + // ] + // }; + initialize: function(dataset) { + // deep copy + this._datasetAsData = $.extend(true, {}, dataset); + _.bindAll(this, 'sync'); + }, + getDataset: function() { + var dataset = new my.Dataset({ + id: this._datasetAsData.metadata.id + }); + // this is a bit weird but problem is in sync this is set to parent model object so need to give dataset a reference to backend explicitly + dataset.backend = this; + return dataset; + }, + sync: function(method, model, options) { + var self = this; + if (method === "read") { + var dfd = $.Deferred(); + // this switching on object type is rather horrible + // think may make more sense to do work in individual objects rather than in central Backbone.sync + if (model.__type__ == 'Dataset') { + var dataset = model; + var rawDataset = this._datasetAsData; + dataset.set(rawDataset.metadata); + dataset.set({ + headers: rawDataset.data.headers + }); + dataset.docCount = rawDataset.data.rows.length; + dfd.resolve(dataset); + } + return dfd.promise(); + } else if (method === 'update') { + var dfd = $.Deferred(); + if (model.__type__ == 'Document') { + _.each(this._datasetAsData.data.rows, function(row, idx) { + if(row.id === model.id) { + self._datasetAsData.data.rows[idx] = model.toJSON(); + } + }); + dfd.resolve(model); + } + return dfd.promise(); + } else if (method === 'delete') { + var dfd = $.Deferred(); + if (model.__type__ == 'Document') { + this._datasetAsData.data.rows = _.reject(this._datasetAsData.data.rows, function(row) { + return (row.id === model.id); + }); + dfd.resolve(model); + } + return dfd.promise(); + } else { + alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model); + } + }, + getDocuments: function(datasetId, numRows, start) { + if (start === undefined) { + start = 0; + } + if (numRows === undefined) { + numRows = 10; + } + var dfd = $.Deferred(); + rows = this._datasetAsData.data.rows; + var results = rows.slice(start, start+numRows); + dfd.resolve(results); + return dfd.promise(); + } +}); + +// Webstore Backend for connecting to the Webstore +// +// Initializing model argument must contain a url attribute pointing to +// relevant Webstore table. +// +// Designed to only attach to one dataset and one dataset only ... +// Could generalize to support attaching to different datasets +my.BackendWebstore = Backbone.Model.extend({ + getDataset: function(id) { + var dataset = new my.Dataset({ + id: id + }); + dataset.backend = this; + return dataset; + }, + sync: function(method, model, options) { + if (method === "read") { + // this switching on object type is rather horrible + // think may make more sense to do work in individual objects rather than in central Backbone.sync + if (this.__type__ == 'Dataset') { + var dataset = this; + // get the schema and return + var base = this.backend.get('url'); + var schemaUrl = base + '/schema.json'; + var jqxhr = $.ajax({ + url: schemaUrl, + dataType: 'jsonp', + jsonp: '_callback' + }); + var dfd = $.Deferred(); + jqxhr.then(function(schema) { + headers = _.map(schema.data, function(item) { + return item.name; + }); + dataset.set({ + headers: headers + }); + dataset.docCount = schema.count; + dfd.resolve(dataset, jqxhr); + }); + return dfd.promise(); + } + } + }, + getDocuments: function(datasetId, numRows, start) { + if (start === undefined) { + start = 0; + } + if (numRows === undefined) { + numRows = 10; + } + var base = this.get('url'); + var jqxhr = $.ajax({ + url: base + '.json?_limit=' + numRows, + dataType: 'jsonp', + jsonp: '_callback', + cache: true + }); + var dfd = $.Deferred(); + jqxhr.then(function(results) { + dfd.resolve(results.data); + }); + return dfd.promise(); + } +}); + +// DataProxy Backend for connecting to the DataProxy +// +// Example initialization: +// +// BackendDataProxy({ +// model: { +// url: {url-of-data-to-proxy}, +// type: xls || csv, +// format: json || jsonp # return format (defaults to jsonp) +// dataproxy: {url-to-proxy} # defaults to http://jsonpdataproxy.appspot.com +// } +// }) +my.BackendDataProxy = Backbone.Model.extend({ + defaults: { + dataproxy: 'http://jsonpdataproxy.appspot.com' + , type: 'csv' + , format: 'jsonp' + }, + getDataset: function(id) { + var dataset = new my.Dataset({ + id: id + }); + dataset.backend = this; + return dataset; + }, + sync: function(method, model, options) { + if (method === "read") { + // this switching on object type is rather horrible + // think may make more sense to do work in individual objects rather than in central Backbone.sync + if (this.__type__ == 'Dataset') { + var dataset = this; + // get the schema and return + var base = this.backend.get('dataproxy'); + var data = this.backend.toJSON(); + delete data['dataproxy']; + // TODO: should we cache for extra efficiency + data['max-results'] = 1; + var jqxhr = $.ajax({ + url: base + , data: data + , dataType: 'jsonp' + }); + var dfd = $.Deferred(); + jqxhr.then(function(results) { + dataset.set({ + headers: results.fields + }); + dfd.resolve(dataset, jqxhr); + }); + return dfd.promise(); + } + } else { + alert('This backend only supports read operations'); + } + }, + getDocuments: function(datasetId, numRows, start) { + if (start === undefined) { + start = 0; + } + if (numRows === undefined) { + numRows = 10; + } + var base = this.get('dataproxy'); + var data = this.toJSON(); + delete data['dataproxy']; + data['max-results'] = numRows; + var jqxhr = $.ajax({ + url: base + , data: data + , dataType: 'jsonp' + // , cache: true + }); + var dfd = $.Deferred(); + jqxhr.then(function(results) { + var _out = _.map(results.data, function(row) { + var tmp = {}; + _.each(results.fields, function(key, idx) { + tmp[key] = row[idx]; + }); + return tmp; + }); + dfd.resolve(_out); + }); + return dfd.promise(); + } +}); + +return my; + +}(jQuery); + +var util = function() { + var templates = { + transformActions: '
  • Global transform...
  • ' + , columnActions: ' \ +
  • Transform...
  • \ +
  • Delete this column
  • \ + ' + , rowActions: '
  • Delete this row
  • ' + , cellEditor: ' \ + \ + ' + , editPreview: ' \ +
    \ + \ + \ + \ + \ + \ + \ + \ + \ + {{#rows}} \ + \ + \ + \ + \ + {{/rows}} \ + \ +
    \ + before \ + \ + after \ +
    \ + {{before}} \ + \ + {{after}} \ +
    \ +
    \ + ' + }; + + $.fn.serializeObject = function() { + var o = {}; + var a = this.serializeArray(); + $.each(a, function() { + if (o[this.name]) { + if (!o[this.name].push) { + o[this.name] = [o[this.name]]; + } + o[this.name].push(this.value || ''); + } else { + o[this.name] = this.value || ''; + } + }); + return o; + }; + + function inURL(url, str) { + var exists = false; + if ( url.indexOf( str ) > -1 ) { + exists = true; + } + return exists; + } + + function registerEmitter() { + var Emitter = function(obj) { + this.emit = function(obj, channel) { + if (!channel) var channel = 'data'; + this.trigger(channel, obj); + }; + }; + MicroEvent.mixin(Emitter); + return new Emitter(); + } + + function listenFor(keys) { + var shortcuts = { // from jquery.hotkeys.js + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + } + window.addEventListener("keyup", function(e) { + var pressed = shortcuts[e.keyCode]; + if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed); + }, false); + } + + function observeExit(elem, callback) { + var cancelButton = elem.find('.cancelButton'); + // TODO: remove (commented out as part of Backbon-i-fication + // app.emitter.on('esc', function() { + // cancelButton.click(); + // app.emitter.clear('esc'); + // }); + cancelButton.click(callback); + } + + function show( thing ) { + $('.' + thing ).show(); + $('.' + thing + '-overlay').show(); + } + + function hide( thing ) { + $('.' + thing ).hide(); + $('.' + thing + '-overlay').hide(); + // TODO: remove or replace (commented out as part of Backbon-i-fication + // if (thing === "dialog") app.emitter.clear('esc'); // todo more elegant solution + } + + function position( thing, elem, offset ) { + var position = $(elem.target).position(); + if (offset) { + if (offset.top) position.top += offset.top; + if (offset.left) position.left += offset.left; + } + $('.' + thing + '-overlay').show().click(function(e) { + $(e.target).hide(); + $('.' + thing).hide(); + }); + $('.' + thing).show().css({top: position.top + $(elem.target).height(), left: position.left}); + } + + function render( template, target, options ) { + if ( !options ) options = {data: {}}; + if ( !options.data ) options = {data: options}; + var html = $.mustache( templates[template], options.data ); + if (target instanceof jQuery) { + var targetDom = target; + } else { + var targetDom = $( "." + target + ":first" ); + } + if( options.append ) { + targetDom.append( html ); + } else { + targetDom.html( html ); + } + // TODO: remove (commented out as part of Backbon-i-fication + // if (template in app.after) app.after[template](); + } + + function notify(message, options) { + if (!options) var options = {}; + var tmplData = _.extend({ + msg: message, + category: 'warning' + }, + options); + var _template = ' \ +
    × \ +

    {{msg}} \ + {{#loader}} \ + \ + {{/loader}} \ +

    \ +
    '; + var _templated = $.mustache(_template, tmplData); + _templated = $(_templated).appendTo($('.data-explorer .alert-messages')); + if (!options.persist) { + setTimeout(function() { + $(_templated).remove(); + }, 3000); + } + } + + function formatMetadata(data) { + out = '
    '; + $.each(data, function(key, val) { + if (typeof(val) == 'string' && key[0] != '_') { + out = out + '
    ' + key + '
    ' + val; + } else if (typeof(val) == 'object' && key != "geometry" && val != null) { + if (key == 'properties') { + $.each(val, function(attr, value){ + out = out + '
    ' + attr + '
    ' + value; + }) + } else { + out = out + '
    ' + key + '
    ' + val.join(', '); + } + } + }); + out = out + '
    '; + return out; + } + + function getBaseURL(url) { + var baseURL = ""; + if ( inURL(url, '_design') ) { + if (inURL(url, '_rewrite')) { + var path = url.split("#")[0]; + if (path[path.length - 1] === "/") { + baseURL = ""; + } else { + baseURL = '_rewrite/'; + } + } else { + baseURL = '_rewrite/'; + } + } + return baseURL; + } + + var persist = { + restore: function() { + $('.persist').each(function(i, el) { + var inputId = $(el).attr('id'); + if(localStorage.getItem(inputId)) $('#' + inputId).val(localStorage.getItem(inputId)); + }) + }, + save: function(id) { + localStorage.setItem(id, $('#' + id).val()); + }, + clear: function() { + $('.persist').each(function(i, el) { + localStorage.removeItem($(el).attr('id')); + }) + } + } + + // simple debounce adapted from underscore.js + function delay(func, wait) { + return function() { + var context = this, args = arguments; + var throttler = function() { + delete app.timeout; + func.apply(context, args); + }; + if (!app.timeout) app.timeout = setTimeout(throttler, wait); + }; + }; + + function resetForm(form) { + $(':input', form) + .not(':button, :submit, :reset, :hidden') + .val('') + .removeAttr('checked') + .removeAttr('selected'); + } + + function largestWidth(selector, min) { + var min_width = min || 0; + $(selector).each(function(i, n){ + var this_width = $(n).width(); + if (this_width > min_width) { + min_width = this_width; + } + }); + return min_width; + } + + function getType(obj) { + if (obj === null) { + return 'null'; + } + if (typeof obj === 'object') { + if (obj.constructor.toString().indexOf("Array") !== -1) { + return 'array'; + } else { + return 'object'; + } + } else { + return typeof obj; + } + } + + function lookupPath(path) { + var docs = app.apiDocs; + try { + _.each(path, function(node) { + docs = docs[node]; + }) + } catch(e) { + util.notify("Error selecting documents" + e); + docs = []; + } + return docs; + } + + function nodePath(docField) { + if (docField.children('.object-key').length > 0) return docField.children('.object-key').text(); + if (docField.children('.array-key').length > 0) return docField.children('.array-key').text(); + if (docField.children('.doc-key').length > 0) return docField.children('.doc-key').text(); + return ""; + } + + function selectedTreePath() { + var nodes = [] + , parent = $('.chosen'); + while (parent.length > 0) { + nodes.push(nodePath(parent)); + parent = parent.parents('.doc-field:first'); + } + return _.compact(nodes).reverse(); + } + + // TODO refactor handlers so that they dont stack up as the tree gets bigger + function handleTreeClick(e) { + var clicked = $(e.target); + if(clicked.hasClass('expand')) return; + if (clicked.children('.array').length > 0) { + var field = clicked; + } else if (clicked.siblings('.array').length > 0) { + var field = clicked.parents('.doc-field:first'); + } else { + var field = clicked.parents('.array').parents('.doc-field:first'); + } + $('.chosen').removeClass('chosen'); + field.addClass('chosen'); + return false; + } + + var createTreeNode = { + "string": function (obj, key) { + var val = $('
    '); + if (obj[key].length > 45) { + val.append($('') + .text(obj[key].slice(0, 45))) + .append( + $('...') + .click(function () { + val.html('') + .append($('') + .text(obj[key].length ? obj[key] : " ") + ) + }) + ) + } + else { + var val = $('
    '); + val.append( + $('') + .text(obj[key].length ? obj[key] : " ") + ) + } + return val; + } + , "number": function (obj, key) { + var val = $('
    ') + val.append($('' + obj[key] + '')) + return val; + } + , "null": function (obj, key) { + var val = $('
    ') + val.append($('' + obj[key] + '')) + return val; + } + , "boolean": function (obj, key) { + var val = $('
    ') + val.append($('' + obj[key] + '')) + return val; + } + , "array": function (obj, key, indent) { + if (!indent) indent = 1; + var val = $('
    ') + $('[...]') + .click(function (e) { + var n = $(this).parent(); + var cls = 'sub-'+key+'-'+indent + n.html('') + n.append('[') + for (i in obj[key]) { + var field = $('
    ').click(handleTreeClick); + n.append( + field + .append('
    '+i+'
    ') + .append(createTreeNode[getType(obj[key][i])](obj[key], i, indent + 1)) + ) + } + n.append(']') + $('div.'+cls).width(largestWidth('div.'+cls)) + }) + .appendTo($('
    ').appendTo(val)) + return val; + } + , "object": function (obj, key, indent) { + if (!indent) indent = 1; + var val = $('
    ') + $('{...}') + .click(function (e) { + var n = $(this).parent(); + n.html('') + n.append('{') + for (i in obj[key]) { + var field = $('
    ').click(handleTreeClick); + var p = $('
    '); + var di = $('
    '+i+'
    ') + field.append(p) + .append(di) + .append(createTreeNode[getType(obj[key][i])](obj[key], i, indent + 1)) + n.append(field) + } + + n.append('}') + di.width(largestWidth('div.object-key')) + }) + .appendTo($('
    ').appendTo(val)) + return val; + } + } + + function renderTree(doc) { + var d = $('div#document-editor'); + for (i in doc) { + var field = $('
    ').click(handleTreeClick); + $('
    ').appendTo(field); + field.append('
    '+i+'
    ') + field.append(createTreeNode[getType(doc[i])](doc, i)); + d.append(field); + } + + $('div.doc-key-base').width(largestWidth('div.doc-key-base')) + } + + + return { + inURL: inURL, + registerEmitter: registerEmitter, + listenFor: listenFor, + show: show, + hide: hide, + position: position, + render: render, + notify: notify, + observeExit: observeExit, + formatMetadata:formatMetadata, + getBaseURL:getBaseURL, + resetForm: resetForm, + delay: delay, + persist: persist, + lookupPath: lookupPath, + selectedTreePath: selectedTreePath, + renderTree: renderTree + }; +}(); +this.recline = this.recline || {}; + +// Views module following classic module pattern +recline.View = function($) { + +var my = {}; + +// Parse a URL query string (?xyz=abc...) into a dictionary. +function parseQueryString(q) { + var urlParams = {}, + e, d = function (s) { + return unescape(s.replace(/\+/g, " ")); + }, + r = /([^&=]+)=?([^&]*)/g; + + if (q && q.length && q[0] === '?') { + q = q.slice(1); + } + while (e = r.exec(q)) { + // TODO: have values be array as query string allow repetition of keys + urlParams[d(e[1])] = d(e[2]); + } + return urlParams; +} + +// The primary view for the entire application. +// +// It should be initialized with a recline.Model.Dataset object and an existing +// dom element to attach to (the existing DOM element is important for +// rendering of FlotGraph subview). +// +// To pass in configuration options use the config key in initialization hash +// e.g. +// +// var explorer = new DataExplorer({ +// config: {...} +// }) +// +// Config options: +// +// * displayCount: how many documents to display initially (default: 10) +// * readOnly: true/false (default: false) value indicating whether to +// operate in read-only mode (hiding all editing options). +// +// All other views as contained in this one. +my.DataExplorer = Backbone.View.extend({ + template: ' \ +
    \ +
    \ + \ +
    \ + \ + \ +
    \ +
    \ + \ + \ +
    \ + ', + + events: { + 'submit form.display-count': 'onDisplayCountUpdate' + }, + + initialize: function(options) { + var self = this; + this.el = $(this.el); + this.config = _.extend({ + displayCount: 10 + , readOnly: false + }, + options.config); + if (this.config.readOnly) { + this.setReadOnly(); + } + // Hash of 'page' views (i.e. those for whole page) keyed by page name + this.pageViews = { + grid: new my.DataTable({ + model: this.model + }) + , graph: new my.FlotGraph({ + model: this.model + }) + }; + // this must be called after pageViews are created + this.render(); + + this.router = new Backbone.Router(); + this.setupRouting(); + + // retrieve basic data like headers etc + // note this.model and dataset returned are the same + this.model.fetch().then(function(dataset) { + self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); + // initialize of dataTable calls render + self.model.getDocuments(self.config.displayCount); + }); + }, + + onDisplayCountUpdate: function(e) { + e.preventDefault(); + this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val()); + this.model.getDocuments(this.config.displayCount); + }, + + setReadOnly: function() { + this.el.addClass('read-only'); + }, + + render: function() { + var tmplData = this.model.toTemplateJSON(); + tmplData.displayCount = this.config.displayCount; + var template = $.mustache(this.template, tmplData); + $(this.el).html(template); + var $dataViewContainer = this.el.find('.data-view-container'); + _.each(this.pageViews, function(view, pageName) { + $dataViewContainer.append(view.el) + }); + }, + + setupRouting: function() { + var self = this; + this.router.route('', 'grid', function() { + self.updateNav('grid'); + }); + this.router.route(/grid(\?.*)?/, 'view', function(queryString) { + self.updateNav('grid', queryString); + }); + this.router.route(/graph(\?.*)?/, 'graph', function(queryString) { + self.updateNav('graph', queryString); + // we have to call here due to fact plot may not have been able to draw + // if it was hidden until now - see comments in FlotGraph.redraw + qsParsed = parseQueryString(queryString); + if ('graph' in qsParsed) { + var chartConfig = JSON.parse(qsParsed['graph']); + _.extend(self.pageViews['graph'].chartConfig, chartConfig); + } + self.pageViews['graph'].redraw(); + }); + }, + + updateNav: function(pageName, queryString) { + this.el.find('.navigation li').removeClass('active'); + var $el = this.el.find('.navigation li a[href=#' + pageName + ']'); + $el.parent().addClass('active'); + // show the specific page + _.each(this.pageViews, function(view, pageViewName) { + if (pageViewName === pageName) { + view.el.show(); + } else { + view.el.hide(); + } + }); + } +}); + +// DataTable provides a tabular view on a Dataset. +// +// Initialize it with a recline.Dataset object. +my.DataTable = Backbone.View.extend({ + tagName: "div", + className: "data-table-container", + + initialize: function() { + var self = this; + this.el = $(this.el); + _.bindAll(this, 'render'); + this.model.currentDocuments.bind('add', this.render); + this.model.currentDocuments.bind('reset', this.render); + this.model.currentDocuments.bind('remove', this.render); + this.state = {}; + }, + + events: { + 'click .column-header-menu': 'onColumnHeaderClick' + , 'click .row-header-menu': 'onRowHeaderClick' + , 'click .data-table-menu li a': 'onMenuClick' + }, + + // TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). + // showDialog: function(template, data) { + // if (!data) data = {}; + // util.show('dialog'); + // util.render(template, 'dialog-content', data); + // util.observeExit($('.dialog-content'), function() { + // util.hide('dialog'); + // }) + // $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + // }, + + + // ====================================================== + // Column and row menus + + onColumnHeaderClick: function(e) { + this.state.currentColumn = $(e.target).siblings().text(); + util.position('data-table-menu', e); + util.render('columnActions', 'data-table-menu'); + }, + + onRowHeaderClick: function(e) { + this.state.currentRow = $(e.target).parents('tr:first').attr('data-id'); + util.position('data-table-menu', e); + util.render('rowActions', 'data-table-menu'); + }, + + onMenuClick: function(e) { + var self = this; + e.preventDefault(); + var actions = { + bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) }, + transform: function() { self.showTransformDialog('transform') }, + // TODO: Delete or re-implement ... + csv: function() { window.location.href = app.csvUrl }, + json: function() { window.location.href = "_rewrite/api/json" }, + urlImport: function() { showDialog('urlImport') }, + pasteImport: function() { showDialog('pasteImport') }, + uploadImport: function() { showDialog('uploadImport') }, + // END TODO + deleteColumn: function() { + var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents."; + // TODO: + alert('This function needs to be re-implemented'); + return; + if (confirm(msg)) costco.deleteColumn(self.state.currentColumn); + }, + deleteRow: function() { + var doc = _.find(self.model.currentDocuments.models, function(doc) { + // important this is == as the currentRow will be string (as comes + // from DOM) while id may be int + return doc.id == self.state.currentRow + }); + doc.destroy().then(function() { + self.model.currentDocuments.remove(doc); + util.notify("Row deleted successfully"); + }) + .fail(function(err) { + util.notify("Errorz! " + err) + }) + } + } + util.hide('data-table-menu'); + actions[$(e.target).attr('data-action')](); + }, + + showTransformColumnDialog: function() { + var $el = $('.dialog-content'); + util.show('dialog'); + var view = new my.ColumnTransform({ + model: this.model + }); + view.state = this.state; + view.render(); + $el.empty(); + $el.append(view.el); + util.observeExit($el, function() { + util.hide('dialog'); + }) + $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + }, + + showTransformDialog: function() { + var $el = $('.dialog-content'); + util.show('dialog'); + var view = new my.DataTransform({ + }); + view.render(); + $el.empty(); + $el.append(view.el); + util.observeExit($el, function() { + util.hide('dialog'); + }) + $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + }, + + + // ====================================================== + // Core Templating + template: ' \ + \ +
      \ + \ + \ + \ + {{#notEmpty}}{{/notEmpty}} \ + {{#headers}} \ + \ + {{/headers}} \ + \ + \ + \ +
      \ +
      \ + \ + {{.}} \ +
      \ + \ +
      \ + ', + + toTemplateJSON: function() { + var modelData = this.model.toJSON() + modelData.notEmpty = ( modelData.headers.length > 0 ) + return modelData; + }, + render: function() { + var self = this; + var htmls = $.mustache(this.template, this.toTemplateJSON()); + this.el.html(htmls); + this.model.currentDocuments.forEach(function(doc) { + var tr = $(''); + self.el.find('tbody').append(tr); + var newView = new my.DataTableRow({ + model: doc, + el: tr, + headers: self.model.get('headers') + }); + newView.render(); + }); + return this; + } +}); + +// DataTableRow View for rendering an individual document. +// +// Since we want this to update in place it is up to creator to provider the element to attach to. +// In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable. +my.DataTableRow = Backbone.View.extend({ + initialize: function(options) { + _.bindAll(this, 'render'); + this._headers = options.headers; + this.el = $(this.el); + this.model.bind('change', this.render); + }, + template: ' \ + \ + {{#cells}} \ + \ +
      \ +   \ +
      {{value}}
      \ +
      \ + \ + {{/cells}} \ + ', + events: { + 'click .data-table-cell-edit': 'onEditClick', + // cell editor + 'click .data-table-cell-editor .okButton': 'onEditorOK', + 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' + }, + + toTemplateJSON: function() { + var doc = this.model; + var cellData = _.map(this._headers, function(header) { + return {header: header, value: doc.get(header)} + }) + return { id: this.id, cells: cellData } + }, + + render: function() { + this.el.attr('data-id', this.model.id); + var html = $.mustache(this.template, this.toTemplateJSON()); + $(this.el).html(html); + return this; + }, + + // ====================================================== + // Cell Editor + + onEditClick: function(e) { + var editing = this.el.find('.data-table-cell-editor-editor'); + if (editing.length > 0) { + editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); + } + $(e.target).addClass("hidden"); + var cell = $(e.target).siblings('.data-table-cell-value'); + cell.data("previousContents", cell.text()); + util.render('cellEditor', cell, {value: cell.text()}); + }, + + onEditorOK: function(e) { + var cell = $(e.target); + var rowId = cell.parents('tr').attr('data-id'); + var header = cell.parents('td').attr('data-header'); + var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val(); + var newData = {}; + newData[header] = newValue; + this.model.set(newData); + util.notify("Updating row...", {loader: true}); + this.model.save().then(function(response) { + util.notify("Row updated successfully", {category: 'success'}); + }) + .fail(function() { + util.notify('Error saving row', { + category: 'error', + persist: true + }); + }); + }, + + onEditorCancel: function(e) { + var cell = $(e.target).parents('.data-table-cell-value'); + cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); + } +}); + + +// View (Dialog) for doing data transformations (on columns of data). +my.ColumnTransform = Backbone.View.extend({ + className: 'transform-column-view', + template: ' \ +
      \ + Functional transform on column {{name}} \ +
      \ +
      \ +
      \ + \ + \ + \ + \ + \ + \ +
      \ +
      \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
      \ + Expression \ +
      \ +
      \ + \ +
      \ +
      \ + No syntax error. \ +
      \ +
      \ + Preview \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ + \ + ', + + events: { + 'click .okButton': 'onSubmit' + , 'keydown .expression-preview-code': 'onEditorKeydown' + }, + + initialize: function() { + this.el = $(this.el); + }, + + render: function() { + var htmls = $.mustache(this.template, + {name: this.state.currentColumn} + ) + this.el.html(htmls); + // Put in the basic (identity) transform script + // TODO: put this into the template? + var editor = this.el.find('.expression-preview-code'); + editor.val("function(doc) {\n doc['"+ this.state.currentColumn+"'] = doc['"+ this.state.currentColumn+"'];\n return doc;\n}"); + editor.focus().get(0).setSelectionRange(18, 18); + editor.keydown(); + }, + + onSubmit: function(e) { + var self = this; + var funcText = this.el.find('.expression-preview-code').val(); + var editFunc = costco.evalFunction(funcText); + if (editFunc.errorMessage) { + util.notify("Error with function! " + editFunc.errorMessage); + return; + } + util.hide('dialog'); + util.notify("Updating all visible docs. This could take a while...", {persist: true, loader: true}); + var docs = self.model.currentDocuments.map(function(doc) { + return doc.toJSON(); + }); + // TODO: notify about failed docs? + var toUpdate = costco.mapDocs(docs, editFunc).edited; + var totalToUpdate = toUpdate.length; + function onCompletedUpdate() { + totalToUpdate += -1; + if (totalToUpdate === 0) { + util.notify(toUpdate.length + " documents updated successfully"); + alert('WARNING: We have only updated the docs in this view. (Updating of all docs not yet implemented!)'); + self.remove(); + } + } + // TODO: Very inefficient as we search through all docs every time! + _.each(toUpdate, function(editedDoc) { + var realDoc = self.model.currentDocuments.get(editedDoc.id); + realDoc.set(editedDoc); + realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate) + }); + }, + + onEditorKeydown: function(e) { + var self = this; + // if you don't setTimeout it won't grab the latest character if you call e.target.value + window.setTimeout( function() { + var errors = self.el.find('.expression-preview-parsing-status'); + var editFunc = costco.evalFunction(e.target.value); + if (!editFunc.errorMessage) { + errors.text('No syntax error.'); + var docs = self.model.currentDocuments.map(function(doc) { + return doc.toJSON(); + }); + var previewData = costco.previewTransform(docs, editFunc, self.state.currentColumn); + util.render('editPreview', 'expression-preview-container', {rows: previewData}); + } else { + errors.text(editFunc.errorMessage); + } + }, 1, true); + } +}); + +// View (Dialog) for doing data transformations on whole dataset. +my.DataTransform = Backbone.View.extend({ + className: 'transform-view', + template: ' \ +
      \ + Recursive transform on all rows \ +
      \ +
      \ +
      \ +

      Traverse and transform objects by visiting every node on a recursive walk using js-traverse.

      \ + \ + \ + \ + \ + \ + \ +
      \ +
      \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
      \ + Expression \ +
      \ +
      \ + \ +
      \ +
      \ + No syntax error. \ +
      \ +
      \ + Preview \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ +
      \ + \ + ', + + initialize: function() { + this.el = $(this.el); + }, + + render: function() { + this.el.html(this.template); + } +}); + + +// Graph view for a Dataset using Flot graphing library. +// +// Initialization arguments: +// +// * model: recline.Model.Dataset +// * config: (optional) graph configuration hash of form: +// +// { +// group: {column name for x-axis}, +// series: [{column name for series A}, {column name series B}, ... ], +// graphType: 'line' +// } +// +// NB: should *not* provide an el argument to the view but must let the view +// generate the element itself (you can then append view.el to the DOM. +my.FlotGraph = Backbone.View.extend({ + + tagName: "div", + className: "data-graph-container", + + template: ' \ +
      \ +
      \ +

      Help »

      \ +

      To create a chart select a column (group) to use as the x-axis \ + then another column (Series A) to plot against it.

      \ +

      You can add add \ + additional series by clicking the "Add series" button

      \ +
      \ +
      \ +
      \ + \ +
      \ + \ +
      \ + \ +
      \ + \ +
      \ +
      \ +
      \ + \ +
      \ + \ +
      \ +
      \ +
      \ +
      \ +
      \ + \ +
      \ + \ +
      \ +
      \ +
      \ +
      \ +', + + events: { + 'change form select': 'onEditorSubmit' + , 'click .editor-add': 'addSeries' + , 'click .action-remove-series': 'removeSeries' + , 'click .action-toggle-help': 'toggleHelp' + }, + + initialize: function(options, config) { + var self = this; + this.el = $(this.el); + _.bindAll(this, 'render', 'redraw'); + // we need the model.headers to render properly + this.model.bind('change', this.render); + this.model.currentDocuments.bind('add', this.redraw); + this.model.currentDocuments.bind('reset', this.redraw); + this.chartConfig = _.extend({ + group: null, + series: [], + graphType: 'line' + }, + config) + this.render(); + }, + + toTemplateJSON: function() { + return this.model.toJSON(); + }, + + render: function() { + htmls = $.mustache(this.template, this.toTemplateJSON()); + $(this.el).html(htmls); + // now set a load of stuff up + this.$graph = this.el.find('.panel.graph'); + // for use later when adding additional series + // could be simpler just to have a common template! + this.$seriesClone = this.el.find('.editor-series').clone(); + this._updateSeries(); + return this; + }, + + onEditorSubmit: function(e) { + var select = this.el.find('.editor-group select'); + this._getEditorData(); + // update navigation + // TODO: make this less invasive (e.g. preserve other keys in query string) + window.location.hash = window.location.hash.split('?')[0] + + '?graph=' + JSON.stringify(this.chartConfig); + this.redraw(); + }, + + redraw: function() { + // There appear to be issues generating a Flot graph if either: + + // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with + // + // Uncaught Invalid dimensions for plot, width = 0, height = 0 + // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' + var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]); + if (!this.plot && (!areWeVisible || this.model.currentDocuments.length == 0)) { + return + } + // create this.plot and cache it + if (!this.plot) { + // only lines for the present + options = { + id: 'line', + name: 'Line Chart' + }; + this.plot = $.plot(this.$graph, this.createSeries(), options); + } + this.plot.setData(this.createSeries()); + this.plot.resize(); + this.plot.setupGrid(); + this.plot.draw(); + }, + + _getEditorData: function() { + $editor = this + var series = this.$series.map(function () { + return $(this).val(); + }); + this.chartConfig.series = $.makeArray(series) + this.chartConfig.group = this.el.find('.editor-group select').val(); + }, + + createSeries: function () { + var self = this; + var series = []; + if (this.chartConfig) { + $.each(this.chartConfig.series, function (seriesIndex, field) { + var points = []; + $.each(self.model.currentDocuments.models, function (index, doc) { + var x = doc.get(self.chartConfig.group); + var y = doc.get(field); + if (typeof x === 'string') { + x = index; + } + points.push([x, y]); + }); + series.push({data: points, label: field}); + }); + } + return series; + }, + + // Public: Adds a new empty series select box to the editor. + // + // All but the first select box will have a remove button that allows them + // to be removed. + // + // Returns itself. + addSeries: function (e) { + e.preventDefault(); + var element = this.$seriesClone.clone(), + label = element.find('label'), + index = this.$series.length; + + this.el.find('.editor-series-group').append(element); + this._updateSeries(); + label.append(' [Remove]'); + label.find('span').text(String.fromCharCode(this.$series.length + 64)); + return this; + }, + + // Public: Removes a series list item from the editor. + // + // Also updates the labels of the remaining series elements. + removeSeries: function (e) { + e.preventDefault(); + var $el = $(e.target); + $el.parent().parent().remove(); + this._updateSeries(); + this.$series.each(function (index) { + if (index > 0) { + var labelSpan = $(this).prev().find('span'); + labelSpan.text(String.fromCharCode(index + 65)); + } + }); + this.onEditorSubmit(); + }, + + toggleHelp: function() { + this.el.find('.editor-info').toggleClass('editor-hide-info'); + }, + + // Private: Resets the series property to reference the select elements. + // + // Returns itself. + _updateSeries: function () { + this.$series = this.el.find('.editor-series select'); + } +}); + +return my; + +}(jQuery); + From 7c782212dc66f915ee3f5deaad72cb948abe3eb8 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 23:16:12 +0000 Subject: [PATCH 43/46] [#1602,js][m]: replace old Data Explorer with new Recline Data Explorer. * Everything working (as far as I can tell using my demo resource data). --- ckan/public/scripts/application.js | 67 +++++++++++++---------- ckan/templates/package/resource_read.html | 13 ++--- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 1b4b98bec7f..53d7be569a5 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -31,7 +31,7 @@ var isResourceView = $('body.package.resource_read').length > 0; if (isResourceView) { - CKANEXT.DATAPREVIEW.setupDataPreview(preload_resource); + CKANEXT.DATAPREVIEW.loadPreviewDialog(preload_resource); } var isDatasetNew = $('body.package.new').length > 0; @@ -742,17 +742,6 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ my.dialogId = 'ckanext-datapreview'; my.$dialog = $('#' + my.dialogId); - // Initialize data explorer on Resource view page - // - // resourceData: resource as simple hash (suitable for initializing backbone model or result of backboneModel.toJSON()) - my.setupDataPreview = function(resourceData) { - // initialize the tableviewer system - DATAEXPLORER.TABLEVIEW.initialize(my.dialogId); - resourceData.formatNormalized = my.normalizeFormat(resourceData.format); - - my.loadPreviewDialog(resourceData); - }; - // **Public: Loads a data preview** // // Fetches the preview data object from the link provided and loads the @@ -763,9 +752,20 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ // // Returns nothing. my.loadPreviewDialog = function(resourceData) { - resourceData.url = my.normalizeUrl(resourceData.url); my.$dialog.html('

      Loading ...

      '); + function initializeDataExplorer(dataset) { + var dataExplorer = new recline.View.DataExplorer({ + el: my.$dialog + , model: dataset + , config: { + readOnly: true + } + }); + // will have to refactor if this can get called multiple times + Backbone.history.start(); + } + // 4 situations // a) have a webstore_url // b) csv or xls (but not webstore) @@ -773,6 +773,9 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ // d) none of the above but worth iframing (assumption is // that if we got here (i.e. preview shown) worth doing // something ...) + resourceData.formatNormalized = my.normalizeFormat(resourceData.format); + + resourceData.url = my.normalizeUrl(resourceData.url); if (resourceData.formatNormalized === '') { var tmp = resourceData.url.split('/'); tmp = tmp[tmp.length - 1]; @@ -785,18 +788,21 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ } if (resourceData.webstore_url) { - var _url = resourceData.webstore_url + '.jsontuples?_limit=500'; - my.getResourceDataDirect(_url, function(data) { - DATAEXPLORER.TABLEVIEW.showData(data); - DATAEXPLORER.TABLEVIEW.$dialog.dialog('open'); + var backend = new recline.Model.BackendWebstore({ + url: resourceData.webstore_url }); + recline.Model.setBackend(backend); + var dataset = backend.getDataset(); + initializeDataExplorer(dataset); } else if (resourceData.formatNormalized in {'csv': '', 'xls': ''}) { - var _url = my.jsonpdataproxyUrl + '?url=' + resourceData.url + '&type=' + resourceData.formatNormalized; - my.getResourceDataDirect(_url, function(data) { - DATAEXPLORER.TABLEVIEW.showData(data); - DATAEXPLORER.TABLEVIEW.$dialog.dialog('open'); + var backend = new recline.Model.BackendDataProxy({ + url: resourceData.url + , type: resourceData.formatNormalized }); + recline.Model.setBackend(backend); + var dataset = backend.getDataset(); + initializeDataExplorer(dataset); } else if (resourceData.formatNormalized in { 'rdf+xml': '', @@ -816,7 +822,6 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ var _url = my.jsonpdataproxyUrl + '?type=csv&url=' + resourceData.url; my.getResourceDataDirect(_url, function(data) { my.showPlainTextData(data); - DATAEXPLORER.TABLEVIEW.$dialog.dialog('open'); }); } else if (resourceData.formatNormalized in {'html':'', 'htm':''} @@ -833,7 +838,10 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ // Cannot reliably preview this item - with no mimetype/format information, // can't guarantee it's not a remote binary file such as an executable. var _msg = $('

      We are unable to preview this type of resource: ' + resourceData.formatNormalized + '

      '); - my.$dialog.html(_msg); + my.showError({ + title: 'Unable to preview' + , message: _msg + }); } }; @@ -884,22 +892,23 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ // // Returns nothing. my.showPlainTextData = function(data) { - // HACK: have to reach into DATAEXPLORER.TABLEVIEW dialog a lot ... - DATAEXPLORER.TABLEVIEW.setupFullscreenDialog(); - if(data.error) { - DATAEXPLORER.TABLEVIEW.showError(data.error); + my.showError(data.error); } else { var content = $('
      ');
             for (var i=0; i
      ' + $.trim(error.message); + my.$dialog.html(_html); + }; + my.normalizeFormat = function(format) { var out = format.toLowerCase(); out = out.split('/'); diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index f7314788614..b3c2ddeeff6 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -13,9 +13,8 @@ - - - + + - - + - - - + From fda5bbb8927f1220598cf2225954c7f3f4443321 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Thu, 26 Jan 2012 23:33:25 +0000 Subject: [PATCH 44/46] [#1709][xs]: correct sql search so it correctly does unfiltered search when passed '*:*' as query. * commiting this fix in master because so tiny (plus had it in #1602 branch but thought better to commit here than there). --- ckan/lib/search/sql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/lib/search/sql.py b/ckan/lib/search/sql.py index d92681e7e24..b33b401b8ee 100644 --- a/ckan/lib/search/sql.py +++ b/ckan/lib/search/sql.py @@ -24,7 +24,7 @@ def run(self, query): def makelike(field): _attr = getattr(model.Package, field) return _attr.ilike('%' + term + '%') - if q and not (q == '""' or q == "''"): + if q and not (q == '""' or q == "''" or q == '*:*'): terms = q.split() # TODO: tags ...? fields = ['name', 'title', 'notes'] From 37b8a0b742ed21c9ec415f77d1f7b62052ae9464 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 27 Jan 2012 10:06:11 +0000 Subject: [PATCH 45/46] [master][#1709]: Fix for simple_search broken on home page and added simple tests. --- ckan/lib/search/sql.py | 2 +- ckan/tests/lib/test_simple_search.py | 35 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 ckan/tests/lib/test_simple_search.py diff --git a/ckan/lib/search/sql.py b/ckan/lib/search/sql.py index d92681e7e24..fc86aa3a096 100644 --- a/ckan/lib/search/sql.py +++ b/ckan/lib/search/sql.py @@ -24,7 +24,7 @@ def run(self, query): def makelike(field): _attr = getattr(model.Package, field) return _attr.ilike('%' + term + '%') - if q and not (q == '""' or q == "''"): + if q and q not in ('""', "''", '*:*'): terms = q.split() # TODO: tags ...? fields = ['name', 'title', 'notes'] diff --git a/ckan/tests/lib/test_simple_search.py b/ckan/tests/lib/test_simple_search.py new file mode 100644 index 00000000000..78a0421c9a9 --- /dev/null +++ b/ckan/tests/lib/test_simple_search.py @@ -0,0 +1,35 @@ +from nose.tools import assert_equal + +from ckan import model +from ckan.lib.create_test_data import CreateTestData +from ckan.lib.search.sql import PackageSearchQuery + +class TestSimpleSearch: + @classmethod + def setup_class(cls): + CreateTestData.create() + + @classmethod + def teardown_class(cls): + model.repo.rebuild_db() + + def test_get_all_entity_ids(self): + ids = PackageSearchQuery().get_all_entity_ids() + anna = model.Package.by_name(u'annakarenina') + assert anna.id in ids + assert len(ids) >= 2, len(ids) + + def test_run_query_basic(self): + res = PackageSearchQuery().run({'q':'annakarenina'}) + anna = model.Package.by_name(u'annakarenina') + assert_equal(res, {'results': [anna.id], 'count': 1}) + + def test_run_query_home(self): + # This is the query from the CKAN home page + res = PackageSearchQuery().run({'q': '*:*'}) + assert res['count'] >= 2, res['count'] + + def test_run_query_all(self): + # This is the default query from the search page + res = PackageSearchQuery().run({'q': u''}) + assert res['count'] >= 2, res['count'] From cb5bba788a8fe0aaab1f88cb673962214e3d7faa Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 27 Jan 2012 10:32:41 +0000 Subject: [PATCH 46/46] [master][#1708][xs]: Protect against import of controller/search code before config initialisation. --- ckan/config/routing.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 63afcc9f4a3..98d6b0f223d 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -8,14 +8,17 @@ from pylons import config from routes import Mapper from ckan.plugins import PluginImplementations, IRoutes -from ckan.controllers.package import register_pluggable_behaviour as register_package_behaviour -from ckan.controllers.group import register_pluggable_behaviour as register_group_behaviour routing_plugins = PluginImplementations(IRoutes) def make_map(): """Create, configure and return the routes Mapper""" + # import controllers here rather than at root level because + # pylons config is initialised by this point. + from ckan.controllers.package import register_pluggable_behaviour as register_package_behaviour + from ckan.controllers.group import register_pluggable_behaviour as register_group_behaviour + map = Mapper(directory=config['pylons.paths']['controllers'], always_scan=config['debug']) map.minimization = False