pair(s)"
+msgstr ""
+
+#: ckan/logic/action/get.py:2078
+msgid "Field \"{field}\" not recognised in resource_search."
+msgstr ""
+
+#: ckan/logic/action/update.py:265 ckan/logic/action/update.py:991
+msgid "Package was not found."
+msgstr ""
+
+#: ckan/logic/action/update.py:308 ckan/logic/action/update.py:526
+#: ckan/logic/action/update.py:1009
+#, python-format
+msgid "REST API: Update object %s"
+msgstr ""
+
+#: ckan/logic/action/update.py:405
+#, python-format
+msgid "REST API: Update package relationship: %s %s %s"
+msgstr ""
+
+#: ckan/logic/action/update.py:766
+msgid "TaskStatus was not found."
+msgstr ""
+
+#: ckan/logic/action/update.py:995
+msgid "Organization was not found."
+msgstr ""
+
+#: ckan/logic/auth/create.py:27 ckan/logic/auth/create.py:45
+#, python-format
+msgid "User %s not authorized to create packages"
+msgstr ""
+
+#: ckan/logic/auth/create.py:31 ckan/logic/auth/update.py:45
+#, python-format
+msgid "User %s not authorized to edit these groups"
+msgstr ""
+
+#: ckan/logic/auth/create.py:38
+#, python-format
+msgid "User %s not authorized to add dataset to this organization"
+msgstr ""
+
+#: ckan/logic/auth/create.py:61
+msgid "No dataset id provided, cannot check auth."
+msgstr ""
+
+#: ckan/logic/auth/create.py:68 ckan/logic/auth/delete.py:34
+#: ckan/logic/auth/get.py:137 ckan/logic/auth/update.py:63
+msgid "No package found for this resource, cannot check auth."
+msgstr ""
+
+#: ckan/logic/auth/create.py:76
+#, python-format
+msgid "User %s not authorized to create resources on dataset %s"
+msgstr ""
+
+#: ckan/logic/auth/create.py:108
+#, python-format
+msgid "User %s not authorized to edit these packages"
+msgstr ""
+
+#: ckan/logic/auth/create.py:119
+#, python-format
+msgid "User %s not authorized to create groups"
+msgstr ""
+
+#: ckan/logic/auth/create.py:129
+#, python-format
+msgid "User %s not authorized to create organizations"
+msgstr ""
+
+#: ckan/logic/auth/create.py:145
+msgid "User {user} not authorized to create users via the API"
+msgstr ""
+
+#: ckan/logic/auth/create.py:148
+msgid "Not authorized to create users"
+msgstr ""
+
+#: ckan/logic/auth/create.py:189
+msgid "Group was not found."
+msgstr ""
+
+#: ckan/logic/auth/create.py:220
+#, python-format
+msgid "User %s not authorized to add members"
+msgstr ""
+
+#: ckan/logic/auth/create.py:244 ckan/logic/auth/update.py:115
+#, python-format
+msgid "User %s not authorized to edit group %s"
+msgstr ""
+
+#: ckan/logic/auth/delete.py:40
+#, python-format
+msgid "User %s not authorized to delete resource %s"
+msgstr ""
+
+#: ckan/logic/auth/delete.py:56
+msgid "Resource view not found, cannot check auth."
+msgstr ""
+
+#: ckan/logic/auth/delete.py:73
+#, python-format
+msgid "User %s not authorized to delete relationship %s"
+msgstr ""
+
+#: ckan/logic/auth/delete.py:82
+#, python-format
+msgid "User %s not authorized to delete groups"
+msgstr ""
+
+#: ckan/logic/auth/delete.py:86
+#, python-format
+msgid "User %s not authorized to delete group %s"
+msgstr ""
+
+#: ckan/logic/auth/delete.py:103
+#, python-format
+msgid "User %s not authorized to delete organizations"
+msgstr ""
+
+#: ckan/logic/auth/delete.py:107
+#, python-format
+msgid "User %s not authorized to delete organization %s"
+msgstr ""
+
+#: ckan/logic/auth/delete.py:120
+#, python-format
+msgid "User %s not authorized to delete task_status"
+msgstr ""
+
+#: ckan/logic/auth/get.py:13 ckan/logic/auth/get.py:270
+msgid "Not authorized"
+msgstr ""
+
+#: ckan/logic/auth/get.py:109
+#, python-format
+msgid "User %s not authorized to read these packages"
+msgstr ""
+
+#: ckan/logic/auth/get.py:124
+#, python-format
+msgid "User %s not authorized to read package %s"
+msgstr ""
+
+#: ckan/logic/auth/get.py:143
+#, python-format
+msgid "User %s not authorized to read resource %s"
+msgstr ""
+
+#: ckan/logic/auth/get.py:170
+#, python-format
+msgid "User %s not authorized to read group %s"
+msgstr ""
+
+#: ckan/logic/auth/get.py:237
+msgid "You must be logged in to access your dashboard."
+msgstr ""
+
+#: ckan/logic/auth/update.py:39
+#, python-format
+msgid "User %s not authorized to edit package %s"
+msgstr ""
+
+#: ckan/logic/auth/update.py:71
+#, python-format
+msgid "User %s not authorized to edit resource %s"
+msgstr ""
+
+#: ckan/logic/auth/update.py:100
+#, python-format
+msgid "User %s not authorized to change state of package %s"
+msgstr ""
+
+#: ckan/logic/auth/update.py:128
+#, python-format
+msgid "User %s not authorized to edit organization %s"
+msgstr ""
+
+#: ckan/logic/auth/update.py:145
+#, python-format
+msgid "User %s not authorized to change state of group %s"
+msgstr ""
+
+#: ckan/logic/auth/update.py:162
+#, python-format
+msgid "User %s not authorized to edit permissions of group %s"
+msgstr ""
+
+#: ckan/logic/auth/update.py:190
+msgid "Have to be logged in to edit user"
+msgstr ""
+
+#: ckan/logic/auth/update.py:198
+#, python-format
+msgid "User %s not authorized to edit user %s"
+msgstr ""
+
+#: ckan/logic/auth/update.py:209
+msgid "User {0} not authorized to update user {1}"
+msgstr ""
+
+#: ckan/logic/auth/update.py:217
+#, python-format
+msgid "User %s not authorized to change state of revision"
+msgstr ""
+
+#: ckan/logic/auth/update.py:226
+#, python-format
+msgid "User %s not authorized to update task_status table"
+msgstr ""
+
+#: ckan/logic/auth/update.py:240
+#, python-format
+msgid "User %s not authorized to update term_translation table"
+msgstr ""
+
+#: ckan/model/license.py:223
+msgid "License not specified"
+msgstr ""
+
+#: ckan/model/license.py:233
+msgid "Open Data Commons Public Domain Dedication and License (PDDL)"
+msgstr ""
+
+#: ckan/model/license.py:243
+msgid "Open Data Commons Open Database License (ODbL)"
+msgstr ""
+
+#: ckan/model/license.py:253
+msgid "Open Data Commons Attribution License"
+msgstr ""
+
+#: ckan/model/license.py:264
+msgid "Creative Commons CCZero"
+msgstr ""
+
+#: ckan/model/license.py:273
+msgid "Creative Commons Attribution"
+msgstr ""
+
+#: ckan/model/license.py:283
+msgid "Creative Commons Attribution Share-Alike"
+msgstr ""
+
+#: ckan/model/license.py:292
+msgid "GNU Free Documentation License"
+msgstr ""
+
+#: ckan/model/license.py:302
+msgid "Other (Open)"
+msgstr ""
+
+#: ckan/model/license.py:312
+msgid "Other (Public Domain)"
+msgstr ""
+
+#: ckan/model/license.py:322
+msgid "Other (Attribution)"
+msgstr ""
+
+#: ckan/model/license.py:334
+msgid "UK Open Government Licence (OGL)"
+msgstr ""
+
+#: ckan/model/license.py:342
+msgid "Creative Commons Non-Commercial (Any)"
+msgstr ""
+
+#: ckan/model/license.py:350
+msgid "Other (Non-Commercial)"
+msgstr ""
+
+#: ckan/model/license.py:358
+msgid "Other (Not Open)"
+msgstr ""
+
+#: ckan/model/package_relationship.py:54
+#, python-format
+msgid "depends on %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:54
+#, python-format
+msgid "is a dependency of %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:55
+#, python-format
+msgid "derives from %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:55
+#, python-format
+msgid "has derivation %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:56
+#, python-format
+msgid "links to %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:56
+#, python-format
+msgid "is linked from %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:57
+#, python-format
+msgid "is a child of %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:57
+#, python-format
+msgid "is a parent of %s"
+msgstr ""
+
+#: ckan/model/package_relationship.py:61
+#, python-format
+msgid "has sibling %s"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/api-info.js:96
+#: ckan/public/base/javascript/modules/api-info.js:96
+msgid "There is no API data to load for this resource"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/api-info.js:124
+#: ckan/public/base/javascript/modules/api-info.js:124
+msgid "Failed to load data API information"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/autocomplete.js:195
+#: ckan/public/base/javascript/modules/autocomplete.js:195
+msgid "Start typing…"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/autocomplete.js:195
+#: ckan/public/base/javascript/modules/autocomplete.js:195
+msgid "No matches found"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/autocomplete.js:204
+#: ckan/public/base/javascript/modules/autocomplete.js:204
+#, python-format
+msgid "Input is too short, must be at least one character"
+msgid_plural "Input is too short, must be at least %(num)d characters"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/public-bs2/base/javascript/modules/basic-form.js:4
+#: ckan/public/base/javascript/modules/basic-form.js:4
+msgid "There are unsaved modifications to this form"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/confirm-action.js:97
+#: ckan/public/base/javascript/modules/confirm-action.js:101
+msgid "Please Confirm Action"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/confirm-action.js:100
+#: ckan/public/base/javascript/modules/confirm-action.js:104
+msgid "Are you sure you want to perform this action?"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/confirm-action.js:102
+#: ckan/public/base/javascript/modules/confirm-action.js:106
+#: ckan/templates/user/new_user_form.html:9
+#: ckan/templates/user/perform_reset.html:26
+msgid "Confirm"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/confirm-action.js:103
+#: ckan/public-bs2/base/javascript/modules/resource-reorder.js:59
+#: ckan/public-bs2/base/javascript/modules/resource-view-reorder.js:53
+#: ckan/public/base/javascript/modules/confirm-action.js:107
+#: ckan/public/base/javascript/modules/resource-reorder.js:59
+#: ckan/public/base/javascript/modules/resource-view-reorder.js:53
+#: ckan/templates/admin/confirm_reset.html:9
+#: ckan/templates/group/confirm_delete.html:14
+#: ckan/templates/group/confirm_delete_member.html:15
+#: ckan/templates/organization/confirm_delete.html:14
+#: ckan/templates/organization/confirm_delete_member.html:15
+#: ckan/templates/package/confirm_delete.html:15
+#: ckan/templates/package/confirm_delete_resource.html:14
+msgid "Cancel"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/follow.js:70
+#: ckan/public/base/javascript/modules/follow.js:70
+#: ckan/templates/snippets/follow_button.html:9
+msgid "Unfollow"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/follow.js:73
+#: ckan/public/base/javascript/modules/follow.js:73
+#: ckan/templates/snippets/follow_button.html:14
+msgid "Follow"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:60
+#: ckan/public/base/javascript/modules/image-upload.js:60
+msgid "Link"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:61
+#: ckan/public/base/javascript/modules/image-upload.js:61
+msgid "Link to a URL on the internet (you can also link to an API)"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:68
+#: ckan/public/base/javascript/modules/image-upload.js:68
+msgid "Upload"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:72
+#: ckan/public/base/javascript/modules/image-upload.js:72
+#: ckan/templates/group/snippets/group_item.html:43
+#: ckan/templates/macros/form.html:241 ckan/templates/snippets/search_form.html:69
+msgid "Remove"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:87
+#: ckan/public/base/javascript/modules/image-upload.js:87
+msgid "Upload a file on your computer"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:110
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:178
+#: ckan/public-bs2/base/javascript/modules/slug-preview.js:56
+#: ckan/public/base/javascript/modules/image-upload.js:110
+#: ckan/public/base/javascript/modules/image-upload.js:178
+#: ckan/public/base/javascript/modules/slug-preview.js:56
+#: ckan/templates/group/snippets/group_form.html:18
+#: ckan/templates/organization/snippets/organization_form.html:18
+#: ckan/templates/package/snippets/package_basic_fields.html:13
+#: ckan/templates/package/snippets/resource_form.html:26
+msgid "URL"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:119
+#: ckan/public-bs2/base/javascript/modules/image-upload.js:209
+#: ckan/public/base/javascript/modules/image-upload.js:119
+#: ckan/public/base/javascript/modules/image-upload.js:209
+msgid "File"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-reorder.js:56
+#: ckan/public-bs2/base/javascript/modules/resource-view-reorder.js:50
+#: ckan/public/base/javascript/modules/resource-reorder.js:56
+#: ckan/public/base/javascript/modules/resource-view-reorder.js:50
+msgid "Save order"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-reorder.js:69
+#: ckan/public-bs2/base/javascript/modules/resource-view-reorder.js:59
+#: ckan/public/base/javascript/modules/resource-reorder.js:69
+#: ckan/public/base/javascript/modules/resource-view-reorder.js:59
+msgid "Saving..."
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-upload-field.js:57
+#: ckan/public/base/javascript/modules/resource-upload-field.js:57
+msgid "Upload a file"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-upload-field.js:144
+#: ckan/public/base/javascript/modules/resource-upload-field.js:144
+msgid "An Error Occurred"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-upload-field.js:201
+#: ckan/public/base/javascript/modules/resource-upload-field.js:201
+msgid "Unable to upload file"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-upload-field.js:252
+#: ckan/public/base/javascript/modules/resource-upload-field.js:252
+msgid "Unable to authenticate upload"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-upload-field.js:260
+#: ckan/public/base/javascript/modules/resource-upload-field.js:260
+msgid "Resource uploaded"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-upload-field.js:266
+#: ckan/public/base/javascript/modules/resource-upload-field.js:266
+msgid "Unable to get data for uploaded file"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-upload-field.js:272
+#: ckan/public/base/javascript/modules/resource-upload-field.js:272
+msgid ""
+"You are uploading a file. Are you sure you want to navigate away and stop "
+"this upload?"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-view-filters.js:9
+#: ckan/public/base/javascript/modules/resource-view-filters.js:9
+#: ckan/templates/package/snippets/view_form_filters.html:16
+msgid "Add Filter"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/resource-view-filters.js:52
+#: ckan/public/base/javascript/modules/resource-view-filters.js:52
+msgid "Select a field"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/slug-preview.js:57
+#: ckan/public/base/javascript/modules/slug-preview.js:57
+#: ckan/templates/group/edit_base.html:20 ckan/templates/group/members.html:28
+#: ckan/templates/organization/bulk_process.html:65
+#: ckan/templates/organization/edit.html:3
+#: ckan/templates/organization/edit_base.html:22
+#: ckan/templates/organization/members.html:33
+#: ckan/templates/package/edit_base.html:11
+#: ckan/templates/package/resource_edit.html:3
+#: ckan/templates/package/resource_edit_base.html:12
+#: ckan/templates/package/snippets/resource_item.html:56
+msgid "Edit"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/table-toggle-more.js:25
+#: ckan/public/base/javascript/modules/table-toggle-more.js:25
+msgid "Show more"
+msgstr ""
+
+#: ckan/public-bs2/base/javascript/modules/table-toggle-more.js:26
+#: ckan/public/base/javascript/modules/table-toggle-more.js:26
+msgid "Hide"
+msgstr ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:13
+#: ckan/public-bs2/base/test/spec/module.spec.js:385
+#: ckan/public/base/test/spec/i18n.spec.js:13
+#: ckan/public/base/test/spec/module.spec.js:385
+msgid "foo"
+msgstr ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:17
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:46
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:50
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:54
+#: ckan/public/base/test/spec/i18n.spec.js:17
+#: ckan/public/base/test/spec/i18n.spec.js:46
+#: ckan/public/base/test/spec/i18n.spec.js:50
+#: ckan/public/base/test/spec/i18n.spec.js:54
+msgid "no translation"
+msgid_plural "no translations"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:22
+#: ckan/public/base/test/spec/i18n.spec.js:22
+#, python-format
+msgid "hello %(name)s!"
+msgstr ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:29
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:76
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:81
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:86
+#: ckan/public/base/test/spec/i18n.spec.js:29
+#: ckan/public/base/test/spec/i18n.spec.js:76
+#: ckan/public/base/test/spec/i18n.spec.js:81
+#: ckan/public/base/test/spec/i18n.spec.js:86
+#, python-format
+msgid "no %(attr)s translation"
+msgid_plural "no %(attr)s translations"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:39
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:40
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:41
+#: ckan/public-bs2/base/test/spec/module.spec.js:395
+#: ckan/public-bs2/base/test/spec/module.spec.js:396
+#: ckan/public-bs2/base/test/spec/module.spec.js:397
+#: ckan/public/base/test/spec/i18n.spec.js:39
+#: ckan/public/base/test/spec/i18n.spec.js:40
+#: ckan/public/base/test/spec/i18n.spec.js:41
+#: ckan/public/base/test/spec/module.spec.js:395
+#: ckan/public/base/test/spec/module.spec.js:396
+#: ckan/public/base/test/spec/module.spec.js:397
+msgid "bar"
+msgid_plural "bars"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:61
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:65
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:69
+#: ckan/public/base/test/spec/i18n.spec.js:61
+#: ckan/public/base/test/spec/i18n.spec.js:65
+#: ckan/public/base/test/spec/i18n.spec.js:69
+#, python-format
+msgid "%(color)s shirt"
+msgid_plural "%(color)s shirts"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:93
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:94
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:95
+#: ckan/public/base/test/spec/i18n.spec.js:93
+#: ckan/public/base/test/spec/i18n.spec.js:94
+#: ckan/public/base/test/spec/i18n.spec.js:95
+#, python-format
+msgid "%(num)d item"
+msgid_plural "%(num)d items"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:100
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:105
+#: ckan/public-bs2/base/test/spec/i18n.spec.js:110
+#: ckan/public/base/test/spec/i18n.spec.js:100
+#: ckan/public/base/test/spec/i18n.spec.js:105
+#: ckan/public/base/test/spec/i18n.spec.js:110
+#, python-format
+msgid "%(num)d missing translation"
+msgid_plural "%(num)d missing translations"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/error_document_template.html:3
+#, python-format
+msgid "Error %(error_code)s"
+msgstr ""
+
+#: ckan/templates/footer.html:9
+msgid "About {0}"
+msgstr ""
+
+#: ckan/templates/footer.html:15
+msgid "CKAN API"
+msgstr ""
+
+#: ckan/templates/footer.html:16
+msgid "CKAN Association"
+msgstr ""
+
+#: ckan/templates/footer.html:24
+msgid ""
+"Powered by CKAN "
+msgstr ""
+
+#: ckan/templates/header.html:9
+msgid "Sysadmin settings"
+msgstr ""
+
+#: ckan/templates/header.html:16
+msgid "View profile"
+msgstr ""
+
+#: ckan/templates/header.html:23
+#, python-format
+msgid "Dashboard (%(num)d new item)"
+msgid_plural "Dashboard (%(num)d new items)"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/header.html:27 ckan/templates/user/dashboard.html:6
+msgid "Dashboard"
+msgstr ""
+
+#: ckan/templates/header.html:33 ckan/templates/user/dashboard.html:16
+msgid "Edit settings"
+msgstr ""
+
+#: ckan/templates/header.html:35
+msgid "Settings"
+msgstr ""
+
+#: ckan/templates/header.html:40 ckan/templates/header.html:42
+msgid "Log out"
+msgstr ""
+
+#: ckan/templates/header.html:52 ckan/templates/user/logout_first.html:14
+msgid "Log in"
+msgstr ""
+
+#: ckan/templates/header.html:54 ckan/templates/user/new.html:3
+msgid "Register"
+msgstr ""
+
+#: ckan/templates/group/read_base.html:17
+#: ckan/templates/group/snippets/info.html:36 ckan/templates/header.html:87
+#: ckan/templates/organization/bulk_process.html:20
+#: ckan/templates/organization/edit_base.html:23
+#: ckan/templates/organization/read_base.html:17 ckan/templates/package/base.html:7
+#: ckan/templates/package/base.html:17 ckan/templates/package/base.html:21
+#: ckan/templates/package/search.html:4
+#: ckan/templates/package/snippets/new_package_breadcrumb.html:1
+#: ckan/templates/revision/diff.html:11 ckan/templates/revision/read.html:65
+#: ckan/templates/snippets/context/group.html:17
+#: ckan/templates/snippets/context/user.html:19
+#: ckan/templates/snippets/organization.html:59 ckan/templates/user/read.html:11
+#: ckan/templates/user/read_base.html:19 ckan/templates/user/read_base.html:53
+msgid "Datasets"
+msgstr ""
+
+#: ckan/templates/header.html:94
+msgid "Search Datasets"
+msgstr ""
+
+#: ckan/templates/header.html:95 ckan/templates/home/snippets/search.html:11
+#: ckan/templates/snippets/simple_search.html:5
+#: ckan/templates/user/snippets/user_search.html:6
+msgid "Search"
+msgstr ""
+
+#: ckan/templates/page.html:6
+msgid "Skip to content"
+msgstr ""
+
+#: ckan/templates/activity_streams/activity_stream_items.html:9
+msgid "Load less"
+msgstr ""
+
+#: ckan/templates/activity_streams/activity_stream_items.html:17
+msgid "Load more"
+msgstr ""
+
+#: ckan/templates/activity_streams/activity_stream_items.html:23
+msgid "No activities are within this activity stream"
+msgstr ""
+
+#: ckan/templates/admin/base.html:3
+msgid "Administration"
+msgstr ""
+
+#: ckan/templates/admin/base.html:8
+msgid "Sysadmins"
+msgstr ""
+
+#: ckan/templates/admin/base.html:9
+msgid "Config"
+msgstr ""
+
+#: ckan/templates/admin/base.html:10 ckan/templates/admin/trash.html:29
+msgid "Trash"
+msgstr ""
+
+#: ckan/templates/admin/config.html:23 ckan/templates/macros/autoform.html:62
+msgid "Site logo"
+msgstr ""
+
+#: ckan/templates/admin/config.html:35 ckan/templates/admin/confirm_reset.html:7
+msgid "Are you sure you want to reset the config?"
+msgstr ""
+
+#: ckan/templates/admin/config.html:35
+msgid "Reset"
+msgstr ""
+
+#: ckan/templates/admin/config.html:36
+msgid "Update Config"
+msgstr ""
+
+#: ckan/templates/admin/config.html:45
+msgid "CKAN config options"
+msgstr ""
+
+#: ckan/templates/admin/config.html:52
+#, python-format
+msgid ""
+" Site Title: This is the title of this CKAN instance It "
+"appears in various places throughout CKAN.
Style: "
+"Choose from a list of simple variations of the main colour scheme to get a "
+"very quick custom theme working.
Site Tag Logo: This "
+"is the logo that appears in the header of all the CKAN instance "
+"templates.
About: This text will appear on this CKAN "
+"instances about page .
Intro "
+"Text: This text will appear on this CKAN instances home page as a welcome to visitors.
"
+"Custom CSS: This is a block of CSS that appears in "
+"<head>
tag of every page. If you wish to customize the "
+"templates more fully we recommend reading the documentation .
"
+"Homepage: This is for choosing a predefined layout for "
+"the modules that appear on your homepage.
"
+msgstr ""
+
+#: ckan/templates/admin/confirm_reset.html:3
+#: ckan/templates/admin/confirm_reset.html:10
+msgid "Confirm Reset"
+msgstr ""
+
+#: ckan/templates/admin/index.html:15
+msgid "Administer CKAN"
+msgstr ""
+
+#: ckan/templates/admin/index.html:20
+#, python-format
+msgid ""
+" As a sysadmin user you have full control over this CKAN instance. Proceed"
+" with care!
For guidance on using sysadmin features, see the CKAN sysadmin guide
"
+msgstr ""
+
+#: ckan/templates/admin/trash.html:20
+msgid "Purge"
+msgstr ""
+
+#: ckan/templates/admin/trash.html:32
+msgid " Purge deleted datasets forever and irreversibly.
"
+msgstr ""
+
+#: ckan/templates/dataviewer/snippets/data_preview.html:9
+msgid "This resource can not be previewed at the moment."
+msgstr ""
+
+#: ckan/templates/dataviewer/snippets/data_preview.html:11
+#: ckan/templates/package/resource_read.html:133
+#: ckan/templates/package/snippets/resource_view.html:34
+msgid "Click here for more information."
+msgstr ""
+
+#: ckan/templates/dataviewer/snippets/data_preview.html:18
+#: ckan/templates/package/snippets/resource_view.html:41
+msgid "Download resource"
+msgstr ""
+
+#: ckan/templates/dataviewer/snippets/no_preview.html:3
+msgid "No preview available."
+msgstr ""
+
+#: ckan/templates/dataviewer/snippets/no_preview.html:5
+msgid "More details..."
+msgstr ""
+
+#: ckan/templates/dataviewer/snippets/no_preview.html:12
+#, python-format
+msgid "No handler defined for data type: %(type)s."
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:5
+msgid "Standard"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:5
+msgid "Standard Input"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:6
+msgid "Medium"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:6
+msgid "Medium Width Input"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:7
+msgid "Full"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:7
+msgid "Full Width Input"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:8
+msgid "Large"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:8
+msgid "Large Input"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:9
+msgid "Prepend"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:9
+msgid "Prepend Input"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:13
+msgid "Custom Field (empty)"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:19
+#: ckan/templates/snippets/custom_form_fields.html:20
+#: ckan/templates/snippets/custom_form_fields.html:37
+msgid "Custom Field"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:22
+msgid "Markdown"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:23
+msgid "Textarea"
+msgstr ""
+
+#: ckan/templates/development/snippets/form.html:24
+msgid "Select"
+msgstr ""
+
+#: ckan/templates/group/activity_stream.html:3
+#: ckan/templates/group/activity_stream.html:6
+#: ckan/templates/group/read_base.html:18
+#: ckan/templates/organization/activity_stream.html:3
+#: ckan/templates/organization/activity_stream.html:6
+#: ckan/templates/organization/read_base.html:18
+#: ckan/templates/package/activity.html:3 ckan/templates/package/activity.html:6
+#: ckan/templates/package/read_base.html:21
+#: ckan/templates/user/activity_stream.html:3
+#: ckan/templates/user/activity_stream.html:6 ckan/templates/user/read_base.html:20
+msgid "Activity Stream"
+msgstr ""
+
+#: ckan/templates/group/admins.html:3 ckan/templates/group/admins.html:6
+#: ckan/templates/organization/admins.html:3
+#: ckan/templates/organization/admins.html:6
+msgid "Administrators"
+msgstr ""
+
+#: ckan/templates/group/base_form_page.html:7
+msgid "Add a Group"
+msgstr ""
+
+#: ckan/templates/group/base_form_page.html:11
+msgid "Group Form"
+msgstr ""
+
+#: ckan/templates/group/confirm_delete.html:3
+#: ckan/templates/group/confirm_delete.html:15
+#: ckan/templates/group/confirm_delete_member.html:3
+#: ckan/templates/group/confirm_delete_member.html:16
+#: ckan/templates/organization/confirm_delete.html:3
+#: ckan/templates/organization/confirm_delete.html:15
+#: ckan/templates/organization/confirm_delete_member.html:3
+#: ckan/templates/organization/confirm_delete_member.html:16
+#: ckan/templates/package/confirm_delete.html:3
+#: ckan/templates/package/confirm_delete.html:16
+#: ckan/templates/package/confirm_delete_resource.html:3
+#: ckan/templates/package/confirm_delete_resource.html:15
+msgid "Confirm Delete"
+msgstr ""
+
+#: ckan/templates/group/confirm_delete.html:11
+msgid "Are you sure you want to delete group - {name}?"
+msgstr ""
+
+#: ckan/templates/group/confirm_delete_member.html:11
+#: ckan/templates/organization/confirm_delete_member.html:11
+msgid "Are you sure you want to delete member - {name}?"
+msgstr ""
+
+#: ckan/templates/group/edit.html:7 ckan/templates/group/edit_base.html:3
+#: ckan/templates/group/edit_base.html:11 ckan/templates/group/read_base.html:12
+#: ckan/templates/organization/edit_base.html:11
+#: ckan/templates/organization/read_base.html:12
+#: ckan/templates/package/read_base.html:14
+#: ckan/templates/package/resource_read.html:31 ckan/templates/user/edit.html:8
+#: ckan/templates/user/edit_base.html:3 ckan/templates/user/read_base.html:14
+msgid "Manage"
+msgstr ""
+
+#: ckan/templates/group/edit.html:12
+msgid "Edit Group"
+msgstr ""
+
+#: ckan/templates/group/edit_base.html:21 ckan/templates/group/members.html:3
+#: ckan/templates/organization/edit_base.html:24
+#: ckan/templates/organization/members.html:3
+msgid "Members"
+msgstr ""
+
+#: ckan/templates/group/history.html:3 ckan/templates/group/history.html:6
+#: ckan/templates/package/history.html:3 ckan/templates/package/history.html:6
+msgid "History"
+msgstr ""
+
+#: ckan/templates/group/index.html:13 ckan/templates/user/dashboard_groups.html:7
+msgid "Add Group"
+msgstr ""
+
+#: ckan/templates/group/index.html:20
+msgid "Search groups..."
+msgstr ""
+
+#: ckan/templates/group/index.html:29
+msgid "There are currently no groups for this site"
+msgstr ""
+
+#: ckan/templates/group/index.html:31 ckan/templates/organization/index.html:31
+msgid "How about creating one?"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:8
+#: ckan/templates/organization/member_new.html:10
+msgid "Back to all members"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:10
+#: ckan/templates/organization/member_new.html:7
+#: ckan/templates/organization/member_new.html:12
+msgid "Edit Member"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:10 ckan/templates/group/member_new.html:64
+#: ckan/templates/group/members.html:6
+#: ckan/templates/organization/member_new.html:7
+#: ckan/templates/organization/member_new.html:12
+#: ckan/templates/organization/member_new.html:63
+#: ckan/templates/organization/members.html:8
+msgid "Add Member"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:18
+#: ckan/templates/organization/member_new.html:19
+msgid "Existing User"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:21
+#: ckan/templates/organization/member_new.html:22
+msgid "If you wish to add an existing user, search for their username below."
+msgstr ""
+
+#: ckan/templates/group/member_new.html:38
+#: ckan/templates/organization/member_new.html:39
+msgid "or"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:42
+#: ckan/templates/organization/member_new.html:43
+msgid "New User"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:45
+#: ckan/templates/organization/member_new.html:46
+msgid "If you wish to invite a new user, enter their email address."
+msgstr ""
+
+#: ckan/templates/group/member_new.html:55 ckan/templates/group/members.html:15
+#: ckan/templates/organization/member_new.html:54
+#: ckan/templates/organization/members.html:20
+msgid "Role"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:58 ckan/templates/group/members.html:31
+#: ckan/templates/organization/member_new.html:57
+#: ckan/templates/organization/members.html:36
+msgid "Are you sure you want to delete this member?"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:58 ckan/templates/group/members.html:31
+#: ckan/templates/group/snippets/group_form.html:38
+#: ckan/templates/organization/bulk_process.html:47
+#: ckan/templates/organization/member_new.html:57
+#: ckan/templates/organization/members.html:36
+#: ckan/templates/organization/snippets/organization_form.html:38
+#: ckan/templates/package/edit_view.html:19
+#: ckan/templates/package/snippets/package_form.html:39
+#: ckan/templates/package/snippets/resource_form.html:67
+#: ckan/templates/revision/read.html:24 ckan/templates/user/edit_user_form.html:45
+msgid "Delete"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:77
+#: ckan/templates/organization/member_new.html:76
+msgid "What are roles?"
+msgstr ""
+
+#: ckan/templates/group/member_new.html:80
+msgid ""
+" Admin: Can edit group information, as well as manage "
+"organization members.
Member: Can add/remove datasets"
+" from groups
"
+msgstr ""
+
+#: ckan/templates/group/new.html:3 ckan/templates/group/new.html:5
+#: ckan/templates/group/new.html:7
+msgid "Create a Group"
+msgstr ""
+
+#: ckan/templates/group/new_group_form.html:17
+msgid "Update Group"
+msgstr ""
+
+#: ckan/templates/group/new_group_form.html:19
+msgid "Create Group"
+msgstr ""
+
+#: ckan/templates/group/read.html:19 ckan/templates/organization/read.html:25
+#: ckan/templates/snippets/search_form.html:3
+msgid "Search datasets..."
+msgstr ""
+
+#: ckan/templates/group/snippets/feeds.html:3
+msgid "Datasets in group: {group}"
+msgstr ""
+
+#: ckan/templates/group/snippets/feeds.html:4
+#: ckan/templates/organization/snippets/feeds.html:4
+msgid "Recent Revision History"
+msgstr ""
+
+#: ckan/templates/group/snippets/group_form.html:10
+#: ckan/templates/organization/snippets/organization_form.html:10
+#: ckan/templates/package/snippets/resource_form.html:30
+msgid "Name"
+msgstr ""
+
+#: ckan/templates/group/snippets/group_form.html:10
+msgid "My Group"
+msgstr ""
+
+#: ckan/templates/group/snippets/group_form.html:18
+msgid "my-group"
+msgstr ""
+
+#: ckan/templates/group/snippets/group_form.html:20
+msgid "A little information about my group..."
+msgstr ""
+
+#: ckan/templates/group/snippets/group_form.html:38
+msgid "Are you sure you want to delete this Group?"
+msgstr ""
+
+#: ckan/templates/group/snippets/group_form.html:41
+msgid "Save Group"
+msgstr ""
+
+#: ckan/templates/group/snippets/group_item.html:38
+#: ckan/templates/group/snippets/group_item.html:39
+msgid "View {name}"
+msgstr ""
+
+#: ckan/templates/group/snippets/group_item.html:43
+msgid "Remove dataset from this group"
+msgstr ""
+
+#: ckan/templates/group/snippets/helper.html:4
+msgid "What are Groups?"
+msgstr ""
+
+#: ckan/templates/group/snippets/helper.html:8
+msgid ""
+" You can use CKAN Groups to create and manage collections of datasets. This "
+"could be to catalogue datasets for a particular project or team, or on a "
+"particular theme, or as a very simple way to help people find and search your"
+" own published datasets. "
+msgstr ""
+
+#: ckan/templates/group/snippets/history_revisions.html:10
+#: ckan/templates/package/snippets/history_revisions.html:10
+msgid "Compare"
+msgstr ""
+
+#: ckan/templates/group/snippets/info.html:16
+#: ckan/templates/organization/bulk_process.html:72
+#: ckan/templates/package/read.html:21
+#: ckan/templates/package/snippets/package_basic_fields.html:118
+#: ckan/templates/snippets/organization.html:37
+#: ckan/templates/snippets/package_item.html:42
+msgid "Deleted"
+msgstr ""
+
+#: ckan/templates/group/snippets/info.html:24
+#: ckan/templates/package/snippets/package_context.html:7
+#: ckan/templates/snippets/organization.html:45
+msgid "read more"
+msgstr ""
+
+#: ckan/templates/group/snippets/revisions_table.html:7
+#: ckan/templates/package/snippets/revisions_table.html:7
+#: ckan/templates/revision/read.html:5 ckan/templates/revision/read.html:9
+#: ckan/templates/revision/read.html:39
+#: ckan/templates/revision/snippets/revisions_list.html:4
+msgid "Revision"
+msgstr ""
+
+#: ckan/templates/group/snippets/revisions_table.html:8
+#: ckan/templates/package/snippets/revisions_table.html:8
+#: ckan/templates/revision/read.html:53
+#: ckan/templates/revision/snippets/revisions_list.html:5
+msgid "Timestamp"
+msgstr ""
+
+#: ckan/templates/group/snippets/revisions_table.html:9
+#: ckan/templates/package/snippets/additional_info.html:25
+#: ckan/templates/package/snippets/additional_info.html:30
+#: ckan/templates/package/snippets/package_metadata_fields.html:14
+#: ckan/templates/package/snippets/revisions_table.html:9
+#: ckan/templates/revision/read.html:50
+#: ckan/templates/revision/snippets/revisions_list.html:6
+msgid "Author"
+msgstr ""
+
+#: ckan/templates/group/snippets/revisions_table.html:10
+#: ckan/templates/package/snippets/revisions_table.html:10
+#: ckan/templates/revision/read.html:56
+#: ckan/templates/revision/snippets/revisions_list.html:8
+msgid "Log Message"
+msgstr ""
+
+#: ckan/templates/home/index.html:4
+msgid "Welcome"
+msgstr ""
+
+#: ckan/templates/home/snippets/about_text.html:1
+msgid ""
+" CKAN is the world’s leading open-source data portal platform.
CKAN"
+" is a complete out-of-the-box software solution that makes data accessible "
+"and usable – by providing tools to streamline publishing, sharing, finding "
+"and using data (including storage of data and provision of robust data APIs)."
+" CKAN is aimed at data publishers (national and regional governments, "
+"companies and organizations) wanting to make their data open and "
+"available.
CKAN is used by governments and user groups worldwide and "
+"powers a variety of official and community data portals including portals for"
+" local, national and international government, such as the UK’s data.gov.uk and the European Union’s publicdata.eu , the Brazilian dados.gov.br , Dutch and Netherland "
+"government portals, as well as city and municipal sites in the US, UK, "
+"Argentina, Finland and elsewhere.
CKAN: http://ckan.org/ CKAN Tour: http://ckan.org/tour/ Features "
+"overview: http://ckan.org/features/
"
+msgstr ""
+
+#: ckan/templates/home/snippets/promoted.html:8
+msgid "Welcome to CKAN"
+msgstr ""
+
+#: ckan/templates/home/snippets/promoted.html:10
+msgid ""
+"This is a nice introductory paragraph about CKAN or the site in general. We "
+"don't have any copy to go here yet but soon we will "
+msgstr ""
+
+#: ckan/templates/home/snippets/promoted.html:19
+msgid "This is a featured section"
+msgstr ""
+
+#: ckan/templates/home/snippets/search.html:2
+msgid "E.g. environment"
+msgstr ""
+
+#: ckan/templates/home/snippets/search.html:6
+msgid "Search data"
+msgstr ""
+
+#: ckan/templates/home/snippets/search.html:8
+msgid "Search datasets"
+msgstr ""
+
+#: ckan/templates/home/snippets/search.html:16
+msgid "Popular tags"
+msgstr ""
+
+#: ckan/templates/home/snippets/stats.html:5
+msgid "{0} statistics"
+msgstr ""
+
+#: ckan/templates/home/snippets/stats.html:11
+msgid "dataset"
+msgstr ""
+
+#: ckan/templates/home/snippets/stats.html:11
+msgid "datasets"
+msgstr ""
+
+#: ckan/templates/home/snippets/stats.html:17
+msgid "organizations"
+msgstr ""
+
+#: ckan/templates/home/snippets/stats.html:23
+msgid "groups"
+msgstr ""
+
+#: ckan/templates/macros/form.html:126
+#, python-format
+msgid ""
+"You can use Markdown formatting here"
+msgstr ""
+
+#: ckan/templates/macros/form.html:277
+msgid "This field is required"
+msgstr ""
+
+#: ckan/templates/macros/form.html:277
+msgid "Custom"
+msgstr ""
+
+#: ckan/templates/macros/form.html:302
+msgid "The form contains invalid entries:"
+msgstr ""
+
+#: ckan/templates/macros/form.html:407
+msgid "Required field"
+msgstr ""
+
+#: ckan/templates/macros/form.html:422
+msgid "http://example.com/my-image.jpg"
+msgstr ""
+
+#: ckan/templates/macros/form.html:423
+msgid "Image URL"
+msgstr ""
+
+#: ckan/templates/macros/form.html:438
+msgid "Clear Upload"
+msgstr ""
+
+#: ckan/templates/organization/base_form_page.html:5
+msgid "Organization Form"
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:3
+#: ckan/templates/organization/bulk_process.html:11
+msgid "Edit datasets"
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:16
+msgid " found for \"{query}\""
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:18
+msgid "Sorry no datasets found for \"{query}\""
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:37
+msgid "Make public"
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:41
+msgid "Make private"
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:70
+#: ckan/templates/package/read.html:18 ckan/templates/snippets/package_item.html:40
+msgid "Draft"
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:75
+#: ckan/templates/package/read.html:11
+#: ckan/templates/package/snippets/package_basic_fields.html:98
+#: ckan/templates/snippets/package_item.html:31
+#: ckan/templates/snippets/private.html:2 ckan/templates/user/read_base.html:82
+#: ckan/templates/user/read_base.html:96
+msgid "Private"
+msgstr ""
+
+#: ckan/templates/organization/bulk_process.html:88
+msgid "This organization has no datasets associated to it"
+msgstr ""
+
+#: ckan/templates/organization/confirm_delete.html:11
+msgid "Are you sure you want to delete organization - {name}?"
+msgstr ""
+
+#: ckan/templates/organization/edit.html:6
+#: ckan/templates/organization/snippets/info.html:13
+#: ckan/templates/organization/snippets/info.html:16
+msgid "Edit Organization"
+msgstr ""
+
+#: ckan/templates/organization/index.html:13
+#: ckan/templates/user/dashboard_organizations.html:7
+msgid "Add Organization"
+msgstr ""
+
+#: ckan/templates/organization/index.html:20
+msgid "Search organizations..."
+msgstr ""
+
+#: ckan/templates/organization/index.html:29
+msgid "There are currently no organizations for this site"
+msgstr ""
+
+#: ckan/templates/organization/member_new.html:31
+#: ckan/templates/user/edit_user_form.html:8
+#: ckan/templates/user/logout_first.html:10
+#: ckan/templates/user/new_user_form.html:5
+#: ckan/templates/user/perform_reset.html:22 ckan/templates/user/read_base.html:76
+#: ckan/templates/user/request_reset.html:16
+#: ckan/templates/user/snippets/login_form.html:20
+msgid "Username"
+msgstr ""
+
+#: ckan/templates/organization/member_new.html:49
+msgid "Email address"
+msgstr ""
+
+#: ckan/templates/organization/member_new.html:59
+msgid "Update Member"
+msgstr ""
+
+#: ckan/templates/organization/member_new.html:79
+msgid ""
+" Admin: Can add/edit and delete datasets, as well as "
+"manage organization members.
Editor: Can add and edit"
+" datasets, but not manage organization members.
"
+"Member: Can view the organization's private datasets, but"
+" not add new datasets.
"
+msgstr ""
+
+#: ckan/templates/organization/members.html:14
+msgid "{count} member"
+msgid_plural "{count} members"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/organization/new.html:3 ckan/templates/organization/new.html:5
+#: ckan/templates/organization/new.html:7 ckan/templates/organization/new.html:12
+msgid "Create an Organization"
+msgstr ""
+
+#: ckan/templates/organization/new_organization_form.html:17
+msgid "Update Organization"
+msgstr ""
+
+#: ckan/templates/organization/new_organization_form.html:19
+msgid "Create Organization"
+msgstr ""
+
+#: ckan/templates/organization/snippets/feeds.html:3
+msgid "Datasets in organization: {group}"
+msgstr ""
+
+#: ckan/templates/organization/snippets/help.html:4
+#: ckan/templates/organization/snippets/helper.html:4
+msgid "What are Organizations?"
+msgstr ""
+
+#: ckan/templates/organization/snippets/help.html:7
+msgid ""
+" Organizations act like publishing departments for datasets (for example, "
+"the Department of Health). This means that datasets can be published by and "
+"belong to a department instead of an individual user.
Within "
+"organizations, admins can assign roles and authorise its members, giving "
+"individual users the right to publish datasets from that particular "
+"organisation (e.g. Office of National Statistics).
"
+msgstr ""
+
+#: ckan/templates/organization/snippets/helper.html:8
+msgid ""
+" CKAN Organizations are used to create, manage and publish collections of "
+"datasets. Users can have different roles within an Organization, depending on"
+" their level of authorisation to create, edit and publish. "
+msgstr ""
+
+#: ckan/templates/organization/snippets/organization_form.html:10
+msgid "My Organization"
+msgstr ""
+
+#: ckan/templates/organization/snippets/organization_form.html:18
+msgid "my-organization"
+msgstr ""
+
+#: ckan/templates/organization/snippets/organization_form.html:20
+msgid "A little information about my organization..."
+msgstr ""
+
+#: ckan/templates/organization/snippets/organization_form.html:38
+msgid ""
+"Are you sure you want to delete this Organization? Note*: Deleting cannot be "
+"performed while public or private datasets belong to this organization."
+msgstr ""
+
+#: ckan/templates/organization/snippets/organization_form.html:41
+msgid "Save Organization"
+msgstr ""
+
+#: ckan/templates/organization/snippets/organization_item.html:42
+#: ckan/templates/organization/snippets/organization_item.html:43
+msgid "View {organization_name}"
+msgstr ""
+
+#: ckan/templates/package/base.html:22 ckan/templates/package/new.html:9
+#: ckan/templates/package/snippets/new_package_breadcrumb.html:2
+msgid "Create Dataset"
+msgstr ""
+
+#: ckan/templates/package/base_form_page.html:22
+msgid "What are datasets?"
+msgstr ""
+
+#: ckan/templates/package/base_form_page.html:25
+msgid ""
+" A CKAN Dataset is a collection of data resources (such as files), together "
+"with a description and other information, at a fixed URL. Datasets are what "
+"users see when searching for data. "
+msgstr ""
+
+#: ckan/templates/package/confirm_delete.html:12
+msgid "Are you sure you want to delete dataset - {name}?"
+msgstr ""
+
+#: ckan/templates/package/confirm_delete_resource.html:11
+msgid "Are you sure you want to delete resource - {name}?"
+msgstr ""
+
+#: ckan/templates/package/edit_base.html:16
+msgid "View dataset"
+msgstr ""
+
+#: ckan/templates/package/edit_base.html:20
+msgid "Edit metadata"
+msgstr ""
+
+#: ckan/templates/package/edit_view.html:3 ckan/templates/package/edit_view.html:4
+#: ckan/templates/package/edit_view.html:8 ckan/templates/package/edit_view.html:12
+msgid "Edit view"
+msgstr ""
+
+#: ckan/templates/package/edit_view.html:20 ckan/templates/package/new_view.html:28
+#: ckan/templates/package/snippets/resource_item.html:32
+msgid "Preview"
+msgstr ""
+
+#: ckan/templates/package/edit_view.html:21
+msgid "Update"
+msgstr ""
+
+#: ckan/templates/package/group_list.html:14
+msgid "Associate this group with this dataset"
+msgstr ""
+
+#: ckan/templates/package/group_list.html:14
+msgid "Add to group"
+msgstr ""
+
+#: ckan/templates/package/group_list.html:23
+msgid "There are no groups associated with this dataset"
+msgstr ""
+
+#: ckan/templates/package/new_package_form.html:15
+msgid "Update Dataset"
+msgstr ""
+
+#: ckan/templates/package/new_resource.html:5
+msgid "Add data to the dataset"
+msgstr ""
+
+#: ckan/templates/package/new_resource.html:11
+#: ckan/templates/package/new_resource_not_draft.html:8
+msgid "Add New Resource"
+msgstr ""
+
+#: ckan/templates/package/new_resource_not_draft.html:3
+#: ckan/templates/package/new_resource_not_draft.html:4
+msgid "Add resource"
+msgstr ""
+
+#: ckan/templates/package/new_resource_not_draft.html:16
+msgid "New resource"
+msgstr ""
+
+#: ckan/templates/package/new_view.html:3 ckan/templates/package/new_view.html:4
+#: ckan/templates/package/new_view.html:8 ckan/templates/package/new_view.html:12
+msgid "Add view"
+msgstr ""
+
+#: ckan/templates/package/new_view.html:19
+msgid ""
+" Data Explorer views may be slow and unreliable unless the DataStore "
+"extension is enabled. For more information, please see the Data Explorer "
+"documentation . "
+msgstr ""
+
+#: ckan/templates/package/new_view.html:29
+#: ckan/templates/package/snippets/resource_form.html:83
+msgid "Add"
+msgstr ""
+
+#: ckan/templates/package/read_base.html:32
+#, python-format
+msgid ""
+"This is an old revision of this dataset, as edited at %(timestamp)s. It may "
+"differ significantly from the current revision ."
+msgstr ""
+
+#: ckan/templates/package/resource_edit_base.html:17
+msgid "All resources"
+msgstr ""
+
+#: ckan/templates/package/resource_edit_base.html:19
+msgid "View resource"
+msgstr ""
+
+#: ckan/templates/package/resource_edit_base.html:24
+#: ckan/templates/package/resource_edit_base.html:30
+msgid "Edit resource"
+msgstr ""
+
+#: ckan/templates/package/resource_edit_base.html:26
+msgid "Views"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:40
+msgid "API Endpoint"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:42
+#: ckan/templates/package/snippets/resource_item.html:47
+msgid "Go to resource"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:44
+#: ckan/templates/package/snippets/resource_item.html:44
+msgid "Download"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:76
+#: ckan/templates/package/resource_read.html:78
+msgid "URL:"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:86
+msgid "From the dataset abstract"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:88
+#, python-format
+msgid "Source: %(dataset)s "
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:127
+msgid "There are no views created for this resource yet."
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:131
+msgid "Not seeing the views you were expecting?"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:136
+msgid "Here are some reasons you may not be seeing expected views:"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:138
+msgid "No view has been created that is suitable for this resource"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:139
+msgid "The site administrators may not have enabled the relevant view plugins"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:140
+msgid ""
+"If a view requires the DataStore, the DataStore plugin may not be enabled, or"
+" the data may not have been pushed to the DataStore, or the DataStore hasn't "
+"finished processing the data yet"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:162
+msgid "Additional Information"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:166
+#: ckan/templates/package/snippets/additional_info.html:6
+#: ckan/templates/revision/diff.html:43
+#: ckan/templates/snippets/additional_info.html:11
+msgid "Field"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:167
+#: ckan/templates/package/snippets/additional_info.html:7
+#: ckan/templates/snippets/additional_info.html:12
+msgid "Value"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:172
+msgid "Data last updated"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:173
+#: ckan/templates/package/resource_read.html:177
+#: ckan/templates/package/resource_read.html:181
+#: ckan/templates/package/resource_read.html:185
+msgid "unknown"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:176
+msgid "Metadata last updated"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:180
+#: ckan/templates/package/snippets/additional_info.html:70
+msgid "Created"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:184
+#: ckan/templates/package/snippets/resource_form.html:39
+#: ckan/templates/package/snippets/resource_info.html:16
+msgid "Format"
+msgstr ""
+
+#: ckan/templates/package/resource_read.html:188
+#: ckan/templates/package/snippets/package_basic_fields.html:30
+#: ckan/templates/snippets/license.html:21
+msgid "License"
+msgstr ""
+
+#: ckan/templates/package/resource_views.html:10
+msgid "New view"
+msgstr ""
+
+#: ckan/templates/package/resource_views.html:28
+msgid "This resource has no views"
+msgstr ""
+
+#: ckan/templates/package/resources.html:8
+msgid "Add new resource"
+msgstr ""
+
+#: ckan/templates/package/resources.html:20
+#: ckan/templates/package/snippets/resources_list.html:26
+#, python-format
+msgid ""
+" This dataset has no data, why not add"
+" some?
"
+msgstr ""
+
+#: ckan/templates/package/search.html:52
+msgid "API"
+msgstr ""
+
+#: ckan/templates/package/search.html:53
+msgid "API Docs"
+msgstr ""
+
+#: ckan/templates/package/search.html:55
+msgid "full {format} dump"
+msgstr ""
+
+#: ckan/templates/package/search.html:56
+#, python-format
+msgid ""
+" You can also access this registry using the %(api_link)s (see "
+"%(api_doc_link)s) or download a %(dump_link)s. "
+msgstr ""
+
+#: ckan/templates/package/search.html:60
+#, python-format
+msgid ""
+" You can also access this registry using the %(api_link)s (see "
+"%(api_doc_link)s). "
+msgstr ""
+
+#: ckan/templates/package/view_edit_base.html:9
+msgid "All views"
+msgstr ""
+
+#: ckan/templates/package/view_edit_base.html:12
+msgid "View view"
+msgstr ""
+
+#: ckan/templates/package/view_edit_base.html:37
+msgid "View preview"
+msgstr ""
+
+#: ckan/templates/package/snippets/additional_info.html:2
+#: ckan/templates/snippets/additional_info.html:7
+msgid "Additional Info"
+msgstr ""
+
+#: ckan/templates/package/snippets/additional_info.html:14
+#: ckan/templates/package/snippets/package_metadata_fields.html:6
+msgid "Source"
+msgstr ""
+
+#: ckan/templates/package/snippets/additional_info.html:37
+#: ckan/templates/package/snippets/additional_info.html:42
+#: ckan/templates/package/snippets/package_metadata_fields.html:20
+msgid "Maintainer"
+msgstr ""
+
+#: ckan/templates/package/snippets/additional_info.html:49
+#: ckan/templates/package/snippets/package_metadata_fields.html:10
+msgid "Version"
+msgstr ""
+
+#: ckan/templates/package/snippets/additional_info.html:56
+#: ckan/templates/package/snippets/package_basic_fields.html:114
+#: ckan/templates/user/read_base.html:91
+msgid "State"
+msgstr ""
+
+#: ckan/templates/package/snippets/additional_info.html:62
+msgid "Last Updated"
+msgstr ""
+
+#: ckan/templates/package/snippets/cannot_create_package.html:10
+msgid "Before you can create a dataset you need to create an organization."
+msgstr ""
+
+#: ckan/templates/package/snippets/cannot_create_package.html:13
+msgid "Create a new organization"
+msgstr ""
+
+#: ckan/templates/package/snippets/cannot_create_package.html:18
+msgid "There are no organizations to which you can assign this dataset."
+msgstr ""
+
+#: ckan/templates/package/snippets/cannot_create_package.html:19
+msgid "Ask a system administrator to create an organization before you can continue."
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:4
+#: ckan/templates/package/snippets/view_form.html:8
+msgid "Title"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:4
+msgid "eg. A descriptive title"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:13
+msgid "eg. my-dataset"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:19
+msgid "eg. Some useful notes about the data"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:24
+msgid "eg. economy, mental health, government"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:45
+msgid ""
+" License definitions and additional information can be found at opendefinition.org "
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:76
+#: ckan/templates/snippets/organization.html:23
+msgid "Organization"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:80
+msgid "No organization"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:95
+msgid "Visibility"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:98
+msgid "Public"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_basic_fields.html:117
+msgid "Active"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_form.html:28
+msgid ""
+"The data license you select above only applies to the contents of any "
+"resource files that you add to this dataset. By submitting this form, you "
+"agree to release the metadata values that you enter into the form "
+"under the Open "
+"Database License ."
+msgstr ""
+
+#: ckan/templates/package/snippets/package_form.html:39
+msgid "Are you sure you want to delete this dataset?"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_form.html:43
+msgid "Next: Add Data"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_metadata_fields.html:6
+msgid "http://example.com/dataset.json"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_metadata_fields.html:10
+msgid "1.0"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_metadata_fields.html:14
+#: ckan/templates/package/snippets/package_metadata_fields.html:20
+#: ckan/templates/user/new_user_form.html:6
+msgid "Joe Bloggs"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_metadata_fields.html:16
+msgid "Author Email"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_metadata_fields.html:16
+#: ckan/templates/package/snippets/package_metadata_fields.html:22
+#: ckan/templates/user/new_user_form.html:7
+msgid "joe@example.com"
+msgstr ""
+
+#: ckan/templates/package/snippets/package_metadata_fields.html:22
+msgid "Maintainer Email"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_edit_form.html:12
+msgid "Update Resource"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:26
+msgid "Data"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:26
+msgid "http://example.com/external-data.csv"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:30
+msgid "eg. January 2011 Gold Prices"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:34
+msgid "Some useful notes about the data"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:39
+msgid "eg. CSV, XML or JSON"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:42
+msgid "This will be guessed automatically. Leave blank if you wish"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:53
+msgid "eg. 2012-06-05"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:55
+msgid "File Size"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:55
+msgid "eg. 1024"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:57
+#: ckan/templates/package/snippets/resource_form.html:59
+msgid "MIME Type"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:57
+#: ckan/templates/package/snippets/resource_form.html:59
+msgid "eg. application/json"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:67
+msgid "Are you sure you want to delete this resource?"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:73
+msgid "Previous"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:76
+msgid "Save & add another"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_form.html:79
+msgid "Finish"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_help.html:2
+msgid "What's a resource?"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_help.html:4
+msgid "A resource can be any file or link to a file containing useful data."
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_item.html:23
+msgid "Explore"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_item.html:35
+msgid "More information"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:10
+msgid "Fullscreen"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:18
+msgid "Embed"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:32
+msgid "This resource view is not available at the moment."
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:74
+msgid "Embed resource view"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:77
+msgid ""
+"You can copy and paste the embed code into a CMS or blog software that "
+"supports raw HTML"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:80
+msgid "Width"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:83
+msgid "Height"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_view.html:86
+msgid "Code"
+msgstr ""
+
+#: ckan/templates/package/snippets/resource_views_list.html:8
+msgid "Resource Preview"
+msgstr ""
+
+#: ckan/templates/package/snippets/resources_list.html:13
+msgid "Data and Resources"
+msgstr ""
+
+#: ckan/templates/package/snippets/resources_list.html:30
+msgid "This dataset has no data"
+msgstr ""
+
+#: ckan/templates/package/snippets/revisions_table.html:24
+#, python-format
+msgid "Read dataset as of %s"
+msgstr ""
+
+#: ckan/templates/package/snippets/stages.html:23
+#: ckan/templates/package/snippets/stages.html:25
+msgid "Create dataset"
+msgstr ""
+
+#: ckan/templates/package/snippets/stages.html:30
+#: ckan/templates/package/snippets/stages.html:34
+#: ckan/templates/package/snippets/stages.html:36
+msgid "Add data"
+msgstr ""
+
+#: ckan/templates/package/snippets/view_form.html:8
+msgid "eg. My View"
+msgstr ""
+
+#: ckan/templates/package/snippets/view_form.html:9
+msgid "eg. Information about my view"
+msgstr ""
+
+#: ckan/templates/package/snippets/view_form_filters.html:28
+msgid "Remove Filter"
+msgstr ""
+
+#: ckan/templates/package/snippets/view_help.html:2
+msgid "What's a view?"
+msgstr ""
+
+#: ckan/templates/package/snippets/view_help.html:4
+msgid "A view is a representation of the data held against a resource"
+msgstr ""
+
+#: ckan/templates/revision/diff.html:6
+msgid "Differences"
+msgstr ""
+
+#: ckan/templates/revision/diff.html:13 ckan/templates/revision/diff.html:18
+#: ckan/templates/revision/diff.html:23
+msgid "Revision Differences"
+msgstr ""
+
+#: ckan/templates/revision/diff.html:44
+msgid "Difference"
+msgstr ""
+
+#: ckan/templates/revision/diff.html:54
+msgid "No Differences"
+msgstr ""
+
+#: ckan/templates/revision/list.html:3 ckan/templates/revision/list.html:6
+#: ckan/templates/revision/list.html:10
+msgid "Revision History"
+msgstr ""
+
+#: ckan/templates/revision/list.html:6 ckan/templates/revision/read.html:8
+msgid "Revisions"
+msgstr ""
+
+#: ckan/templates/revision/read.html:30
+msgid "Undelete"
+msgstr ""
+
+#: ckan/templates/revision/read.html:64
+msgid "Changes"
+msgstr ""
+
+#: ckan/templates/revision/read.html:74
+msgid "Datasets' Tags"
+msgstr ""
+
+#: ckan/templates/revision/snippets/revisions_list.html:7
+msgid "Entity"
+msgstr ""
+
+#: ckan/templates/snippets/activity_item.html:3
+msgid "New activity item"
+msgstr ""
+
+#: ckan/templates/snippets/add_dataset.html:6
+msgid "Add Dataset"
+msgstr ""
+
+#: ckan/templates/snippets/datapusher_status.html:8
+msgid "Datapusher status: {status}."
+msgstr ""
+
+#: ckan/templates/snippets/disqus_trackback.html:2
+msgid "Trackback URL"
+msgstr ""
+
+#: ckan/templates/snippets/facet_list.html:82
+msgid "Show More {facet_type}"
+msgstr ""
+
+#: ckan/templates/snippets/facet_list.html:85
+msgid "Show Only Popular {facet_type}"
+msgstr ""
+
+#: ckan/templates/snippets/facet_list.html:89
+msgid "There are no {facet_type} that match this search"
+msgstr ""
+
+#: ckan/templates/snippets/home_breadcrumb_item.html:2
+msgid "Home"
+msgstr ""
+
+#: ckan/templates/snippets/language_selector.html:3
+msgid "Language"
+msgstr ""
+
+#: ckan/templates/snippets/language_selector.html:11
+#: ckan/templates/snippets/search_form.html:42
+#: ckan/templates/snippets/simple_search.html:15
+#: ckan/templates/snippets/sort_by.html:22
+msgid "Go"
+msgstr ""
+
+#: ckan/templates/snippets/license.html:14
+msgid "No License Provided"
+msgstr ""
+
+#: ckan/templates/snippets/license.html:28
+msgid "This dataset satisfies the Open Definition."
+msgstr ""
+
+#: ckan/templates/snippets/organization.html:48
+msgid "There is no description for this organization"
+msgstr ""
+
+#: ckan/templates/snippets/package_item.html:57
+msgid "This dataset has no description"
+msgstr ""
+
+#: ckan/templates/snippets/search_form.html:33
+#: ckan/templates/snippets/simple_search.html:8
+#: ckan/templates/snippets/sort_by.html:12
+msgid "Order by"
+msgstr ""
+
+#: ckan/templates/snippets/search_form.html:74
+msgid "Filter Results"
+msgstr ""
+
+#: ckan/templates/snippets/search_form.html:81
+msgid " "
+msgstr ""
+
+#: ckan/templates/snippets/search_form.html:87
+msgid ""
+" There was an error while searching. "
+"Please try again.
"
+msgstr ""
+
+#: ckan/templates/snippets/search_result_text.html:15
+msgid "{number} dataset found for \"{query}\""
+msgid_plural "{number} datasets found for \"{query}\""
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/snippets/search_result_text.html:16
+msgid "No datasets found for \"{query}\""
+msgstr ""
+
+#: ckan/templates/snippets/search_result_text.html:17
+msgid "{number} dataset found"
+msgid_plural "{number} datasets found"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/snippets/search_result_text.html:18
+msgid "No datasets found"
+msgstr ""
+
+#: ckan/templates/snippets/search_result_text.html:21
+msgid "{number} group found for \"{query}\""
+msgid_plural "{number} groups found for \"{query}\""
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/snippets/search_result_text.html:22
+msgid "No groups found for \"{query}\""
+msgstr ""
+
+#: ckan/templates/snippets/search_result_text.html:23
+msgid "{number} group found"
+msgid_plural "{number} groups found"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/snippets/search_result_text.html:24
+msgid "No groups found"
+msgstr ""
+
+#: ckan/templates/snippets/search_result_text.html:27
+msgid "{number} organization found for \"{query}\""
+msgid_plural "{number} organizations found for \"{query}\""
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/snippets/search_result_text.html:28
+msgid "No organizations found for \"{query}\""
+msgstr ""
+
+#: ckan/templates/snippets/search_result_text.html:29
+msgid "{number} organization found"
+msgid_plural "{number} organizations found"
+msgstr[0] ""
+msgstr[1] ""
+
+#: ckan/templates/snippets/search_result_text.html:30
+msgid "No organizations found"
+msgstr ""
+
+#: ckan/templates/snippets/social.html:5
+msgid "Social"
+msgstr ""
+
+#: ckan/templates/snippets/subscribe.html:2
+msgid "Subscribe"
+msgstr ""
+
+#: ckan/templates/snippets/subscribe.html:4
+#: ckan/templates/user/edit_user_form.html:12
+#: ckan/templates/user/new_user_form.html:7 ckan/templates/user/read_base.html:82
+msgid "Email"
+msgstr ""
+
+#: ckan/templates/snippets/subscribe.html:5
+msgid "RSS"
+msgstr ""
+
+#: ckan/templates/snippets/context/user.html:23
+#: ckan/templates/user/read_base.html:57
+msgid "Edits"
+msgstr ""
+
+#: ckan/templates/tag/index.html:33 ckan/templates/tag/index.html:34
+msgid "Search Tags"
+msgstr ""
+
+#: ckan/templates/user/dashboard.html:19 ckan/templates/user/dashboard.html:37
+msgid "News feed"
+msgstr ""
+
+#: ckan/templates/user/dashboard.html:20
+#: ckan/templates/user/dashboard_datasets.html:12
+msgid "My Datasets"
+msgstr ""
+
+#: ckan/templates/user/dashboard.html:21
+#: ckan/templates/user/dashboard_organizations.html:12
+msgid "My Organizations"
+msgstr ""
+
+#: ckan/templates/user/dashboard.html:22
+#: ckan/templates/user/dashboard_groups.html:12
+msgid "My Groups"
+msgstr ""
+
+#: ckan/templates/user/dashboard.html:39
+msgid "Activity from items that I'm following"
+msgstr ""
+
+#: ckan/templates/user/dashboard_datasets.html:17 ckan/templates/user/read.html:20
+msgid "You haven't created any datasets."
+msgstr ""
+
+#: ckan/templates/user/dashboard_datasets.html:19
+#: ckan/templates/user/dashboard_groups.html:22
+#: ckan/templates/user/dashboard_organizations.html:23
+#: ckan/templates/user/read.html:22
+msgid "Create one now?"
+msgstr ""
+
+#: ckan/templates/user/dashboard_groups.html:20
+msgid "You are not a member of any groups."
+msgstr ""
+
+#: ckan/templates/user/dashboard_organizations.html:21
+msgid "You are not a member of any organizations."
+msgstr ""
+
+#: ckan/templates/user/edit.html:6 ckan/templates/user/edit_base.html:3
+#: ckan/templates/user/list.html:6 ckan/templates/user/list.html:13
+#: ckan/templates/user/read_base.html:5 ckan/templates/user/read_base.html:8
+#: ckan/templates/user/snippets/user_search.html:2
+msgid "Users"
+msgstr ""
+
+#: ckan/templates/user/edit.html:17
+msgid "Account Info"
+msgstr ""
+
+#: ckan/templates/user/edit.html:19
+msgid " Your profile lets other CKAN users know about who you are and what you do. "
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:7
+msgid "Change details"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:10
+msgid "Full name"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:10
+msgid "eg. Joe Bloggs"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:12
+msgid "eg. joe@example.com"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:14
+msgid "A little information about yourself"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:17
+msgid "Subscribe to notification emails"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:26
+msgid "Change password"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:29
+msgid "Sysadmin Password"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:37
+#: ckan/templates/user/logout_first.html:11
+#: ckan/templates/user/new_user_form.html:8
+#: ckan/templates/user/perform_reset.html:25
+#: ckan/templates/user/snippets/login_form.html:22
+msgid "Password"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:39
+msgid "Confirm Password"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:45
+msgid "Are you sure you want to delete this User?"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:50
+msgid "Are you sure you want to regenerate the API key?"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:50
+msgid "Regenerate API Key"
+msgstr ""
+
+#: ckan/templates/user/edit_user_form.html:54
+msgid "Update Profile"
+msgstr ""
+
+#: ckan/templates/user/list.html:3 ckan/templates/user/snippets/user_search.html:11
+msgid "All Users"
+msgstr ""
+
+#: ckan/templates/user/login.html:3 ckan/templates/user/login.html:6
+#: ckan/templates/user/login.html:12
+#: ckan/templates/user/snippets/login_form.html:28
+msgid "Login"
+msgstr ""
+
+#: ckan/templates/user/login.html:25
+msgid "Need an Account?"
+msgstr ""
+
+#: ckan/templates/user/login.html:27
+msgid "Then sign right up, it only takes a minute."
+msgstr ""
+
+#: ckan/templates/user/login.html:30
+msgid "Create an Account"
+msgstr ""
+
+#: ckan/templates/user/login.html:42
+msgid "Forgotten your password?"
+msgstr ""
+
+#: ckan/templates/user/login.html:44
+msgid "No problem, use our password recovery form to reset it."
+msgstr ""
+
+#: ckan/templates/user/login.html:47
+msgid "Forgot your password?"
+msgstr ""
+
+#: ckan/templates/user/logout.html:3 ckan/templates/user/logout.html:9
+msgid "Logged Out"
+msgstr ""
+
+#: ckan/templates/user/logout.html:11
+msgid "You are now logged out."
+msgstr ""
+
+#: ckan/templates/user/logout_first.html:9
+msgid "You're already logged in as {user}."
+msgstr ""
+
+#: ckan/templates/user/logout_first.html:9
+msgid "Logout"
+msgstr ""
+
+#: ckan/templates/user/logout_first.html:12
+#: ckan/templates/user/snippets/login_form.html:24
+msgid "Remember me"
+msgstr ""
+
+#: ckan/templates/user/logout_first.html:20
+msgid "You're already logged in"
+msgstr ""
+
+#: ckan/templates/user/logout_first.html:22
+msgid "You need to log out before you can log in with another account."
+msgstr ""
+
+#: ckan/templates/user/logout_first.html:23
+msgid "Log out now"
+msgstr ""
+
+#: ckan/templates/user/new.html:6
+msgid "Registration"
+msgstr ""
+
+#: ckan/templates/user/new.html:14
+msgid "Register for an Account"
+msgstr ""
+
+#: ckan/templates/user/new.html:26
+msgid "Why Sign Up?"
+msgstr ""
+
+#: ckan/templates/user/new.html:28
+msgid "Create datasets, groups and other exciting things"
+msgstr ""
+
+#: ckan/templates/user/new_user_form.html:5
+msgid "username"
+msgstr ""
+
+#: ckan/templates/user/new_user_form.html:6
+msgid "Full Name"
+msgstr ""
+
+#: ckan/templates/user/new_user_form.html:19
+msgid "Create Account"
+msgstr ""
+
+#: ckan/templates/user/perform_reset.html:4
+#: ckan/templates/user/perform_reset.html:14
+msgid "Reset Your Password"
+msgstr ""
+
+#: ckan/templates/user/perform_reset.html:7
+msgid "Password Reset"
+msgstr ""
+
+#: ckan/templates/user/perform_reset.html:21
+msgid "You can also change username. It can not be modified later."
+msgstr ""
+
+#: ckan/templates/user/perform_reset.html:29
+msgid "Update Password"
+msgstr ""
+
+#: ckan/templates/user/perform_reset.html:43
+#: ckan/templates/user/request_reset.html:32
+msgid "How does this work?"
+msgstr ""
+
+#: ckan/templates/user/perform_reset.html:45
+msgid "Simply enter a new password and we'll update your account"
+msgstr ""
+
+#: ckan/templates/user/read.html:27
+msgid "User hasn't created any datasets."
+msgstr ""
+
+#: ckan/templates/user/read_base.html:39
+msgid "You have not provided a biography."
+msgstr ""
+
+#: ckan/templates/user/read_base.html:41
+msgid "This user has no biography."
+msgstr ""
+
+#: ckan/templates/user/read_base.html:73
+msgid "Open ID"
+msgstr ""
+
+#: ckan/templates/user/read_base.html:82 ckan/templates/user/read_base.html:96
+msgid "This means only you can see this"
+msgstr ""
+
+#: ckan/templates/user/read_base.html:87
+msgid "Member Since"
+msgstr ""
+
+#: ckan/templates/user/read_base.html:96
+msgid "API Key"
+msgstr ""
+
+#: ckan/templates/user/request_reset.html:3
+#: ckan/templates/user/request_reset.html:13
+msgid "Reset your password"
+msgstr ""
+
+#: ckan/templates/user/request_reset.html:6
+msgid "Password reset"
+msgstr ""
+
+#: ckan/templates/user/request_reset.html:19
+msgid "Request reset"
+msgstr ""
+
+#: ckan/templates/user/request_reset.html:34
+msgid ""
+"Enter your username into the box and we will send you an email with a link to"
+" enter a new password."
+msgstr ""
+
+#: ckan/templates/user/snippets/followee_dropdown.html:15
+#: ckan/templates/user/snippets/followee_dropdown.html:16
+msgid "Activity from:"
+msgstr ""
+
+#: ckan/templates/user/snippets/followee_dropdown.html:23
+msgid "Search list..."
+msgstr ""
+
+#: ckan/templates/user/snippets/followee_dropdown.html:44
+msgid "You are not following anything"
+msgstr ""
+
+#: ckan/templates/user/snippets/followers.html:9
+msgid "No followers"
+msgstr ""
+
+#: ckan/templates/user/snippets/user_search.html:5
+msgid "Search Users"
+msgstr ""
+
+#: ckan/views/user.py:588
+msgid "Your password must be 8 characters or longer."
+msgstr ""
+
diff --git a/venv/lib/python2.7/site-packages/ckan/include/__init__.py b/venv/lib/python2.7/site-packages/ckan/include/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/venv/lib/python2.7/site-packages/ckan/include/rcssmin.py b/venv/lib/python2.7/site-packages/ckan/include/rcssmin.py
new file mode 100644
index 00000000..3f0435f0
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/include/rcssmin.py
@@ -0,0 +1,360 @@
+#!/usr/bin/env python
+# -*- coding: ascii -*-
+#
+# Copyright 2011, 2012
+# Andr\xe9 Malo or his licensors, as applicable
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+r"""
+==============
+ CSS Minifier
+==============
+
+CSS Minifier.
+
+The minifier is based on the semantics of the `YUI compressor`_\, which itself
+is based on `the rule list by Isaac Schlueter`_\.
+
+This module is a re-implementation aiming for speed instead of maximum
+compression, so it can be used at runtime (rather than during a preprocessing
+step). RCSSmin does syntactical compression only (removing spaces, comments
+and possibly semicolons). It does not provide semantic compression (like
+removing empty blocks, collapsing redundant properties etc). It does, however,
+support various CSS hacks (by keeping them working as intended).
+
+Here's a feature list:
+
+- Strings are kept, except that escaped newlines are stripped
+- Space/Comments before the very end or before various characters are
+ stripped: ``:{});=>+],!`` (The colon (``:``) is a special case, a single
+ space is kept if it's outside a ruleset.)
+- Space/Comments at the very beginning or after various characters are
+ stripped: ``{}(=:>+[,!``
+- Optional space after unicode escapes is kept, resp. replaced by a simple
+ space
+- whitespaces inside ``url()`` definitions are stripped
+- Comments starting with an exclamation mark (``!``) can be kept optionally.
+- All other comments and/or whitespace characters are replaced by a single
+ space.
+- Multiple consecutive semicolons are reduced to one
+- The last semicolon within a ruleset is stripped
+- CSS Hacks supported:
+
+ - IE7 hack (``>/**/``)
+ - Mac-IE5 hack (``/*\*/.../**/``)
+ - The boxmodelhack is supported naturally because it relies on valid CSS2
+ strings
+ - Between ``:first-line`` and the following comma or curly brace a space is
+ inserted. (apparently it's needed for IE6)
+ - Same for ``:first-letter``
+
+rcssmin.c is a reimplementation of rcssmin.py in C and improves runtime up to
+factor 50 or so (depending on the input).
+
+Both python 2 (>= 2.4) and python 3 are supported.
+
+.. _YUI compressor: https://github.com/yui/yuicompressor/
+
+.. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/tree/
+"""
+__author__ = "Andr\xe9 Malo"
+__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')
+__docformat__ = "restructuredtext en"
+__license__ = "Apache License, Version 2.0"
+__version__ = '1.0.0'
+__all__ = ['cssmin']
+
+import re as _re
+
+
+def _make_cssmin(python_only=False):
+ """
+ Generate CSS minifier.
+
+ :Parameters:
+ `python_only` : ``bool``
+ Use only the python variant. If true, the c extension is not even
+ tried to be loaded.
+
+ :Return: Minifier
+ :Rtype: ``callable``
+ """
+ # pylint: disable = W0612
+ # ("unused" variables)
+
+ # pylint: disable = R0911, R0912, R0914, R0915
+ # (too many anything)
+
+ if not python_only:
+ try:
+ import _rcssmin
+ except ImportError:
+ pass
+ else:
+ return _rcssmin.cssmin
+
+ nl = r'(?:[\n\f]|\r\n?)' # pylint: disable = C0103
+ spacechar = r'[\r\n\f\040\t]'
+
+ unicoded = r'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?'
+ escaped = r'[^\n\r\f0-9a-fA-F]'
+ escape = r'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals()
+
+ nmchar = r'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]'
+ #nmstart = r'[^\000-\100\133-\136\140\173-\177]'
+ #ident = (r'(?:'
+ # r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*'
+ #r')') % locals()
+
+ comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
+
+ # only for specific purposes. The bang is grouped:
+ _bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
+
+ string1 = \
+ r'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)'
+ string2 = r'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")'
+ strings = r'(?:%s|%s)' % (string1, string2)
+
+ nl_string1 = \
+ r'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)'
+ nl_string2 = r'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")'
+ nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2)
+
+ uri_nl_string1 = r'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)'
+ uri_nl_string2 = r'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")'
+ uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2)
+
+ nl_escaped = r'(?:\\%(nl)s)' % locals()
+
+ space = r'(?:%(spacechar)s|%(comment)s)' % locals()
+
+ ie7hack = r'(?:>/\*\*/)'
+
+ uri = (r'(?:'
+ r'(?:[^\000-\040"\047()\\\177]*'
+ r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)'
+ r'(?:'
+ r'(?:%(spacechar)s+|%(nl_escaped)s+)'
+ r'(?:'
+ r'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)'
+ r'[^\000-\040"\047()\\\177]*'
+ r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*'
+ r')+'
+ r')*'
+ r')') % locals()
+
+ nl_unesc_sub = _re.compile(nl_escaped).sub
+
+ uri_space_sub = _re.compile((
+ r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'
+ ) % locals()).sub
+ uri_space_subber = lambda m: m.groups()[0] or ''
+
+ space_sub_simple = _re.compile((
+ r'[\r\n\f\040\t;]+|(%(comment)s+)'
+ ) % locals()).sub
+ space_sub_banged = _re.compile((
+ r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)'
+ ) % locals()).sub
+
+ post_esc_sub = _re.compile(r'[\r\n\f\t]+').sub
+
+ main_sub = _re.compile((
+ r'([^\\"\047u>@\r\n\f\040\t/;:{}]+)'
+ r'|(?<=[{}(=:>+[,!])(%(space)s+)'
+ r'|^(%(space)s+)'
+ r'|(%(space)s+)(?=(([:{});=>+\],!])|$)?)'
+ r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)'
+ r'|(\{)'
+ r'|(\})'
+ r'|(%(strings)s)'
+ r'|(?@\r\n\f\040\t/;:{}]*)'
+ ) % locals()).sub
+
+ #print main_sub.__self__.pattern
+
+ def main_subber(keep_bang_comments):
+ """ Make main subber """
+ in_macie5, in_rule, at_media = [0], [0], [0]
+
+ if keep_bang_comments:
+ space_sub = space_sub_banged
+ def space_subber(match):
+ """ Space|Comment subber """
+ if match.lastindex:
+ group1, group2 = match.group(1, 2)
+ if group2:
+ if group1.endswith(r'\*/'):
+ in_macie5[0] = 1
+ else:
+ in_macie5[0] = 0
+ return group1
+ elif group1:
+ if group1.endswith(r'\*/'):
+ if in_macie5[0]:
+ return ''
+ in_macie5[0] = 1
+ return r'/*\*/'
+ elif in_macie5[0]:
+ in_macie5[0] = 0
+ return '/**/'
+ return ''
+ else:
+ space_sub = space_sub_simple
+ def space_subber(match):
+ """ Space|Comment subber """
+ if match.lastindex:
+ if match.group(1).endswith(r'\*/'):
+ if in_macie5[0]:
+ return ''
+ in_macie5[0] = 1
+ return r'/*\*/'
+ elif in_macie5[0]:
+ in_macie5[0] = 0
+ return '/**/'
+ return ''
+
+ def fn_space_post(group):
+ """ space with token after """
+ if group(5) is None or (
+ group(6) == ':' and not in_rule[0] and not at_media[0]):
+ return ' ' + space_sub(space_subber, group(4))
+ return space_sub(space_subber, group(4))
+
+ def fn_semicolon(group):
+ """ ; handler """
+ return ';' + space_sub(space_subber, group(7))
+
+ def fn_semicolon2(group):
+ """ ; handler """
+ if in_rule[0]:
+ return space_sub(space_subber, group(7))
+ return ';' + space_sub(space_subber, group(7))
+
+ def fn_open(group):
+ """ { handler """
+ # pylint: disable = W0613
+ if at_media[0]:
+ at_media[0] -= 1
+ else:
+ in_rule[0] = 1
+ return '{'
+
+ def fn_close(group):
+ """ } handler """
+ # pylint: disable = W0613
+ in_rule[0] = 0
+ return '}'
+
+ def fn_media(group):
+ """ @media handler """
+ at_media[0] += 1
+ return group(13)
+
+ def fn_ie7hack(group):
+ """ IE7 Hack handler """
+ if not in_rule[0] and not at_media[0]:
+ in_macie5[0] = 0
+ return group(14) + space_sub(space_subber, group(15))
+ return '>' + space_sub(space_subber, group(15))
+
+ table = (
+ None,
+ None,
+ None,
+ None,
+ fn_space_post, # space with token after
+ fn_space_post, # space with token after
+ fn_space_post, # space with token after
+ fn_semicolon, # semicolon
+ fn_semicolon2, # semicolon
+ fn_open, # {
+ fn_close, # }
+ lambda g: g(11), # string
+ lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),
+ # url(...)
+ fn_media, # @media
+ None,
+ fn_ie7hack, # ie7hack
+ None,
+ lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),
+ # :first-line|letter followed
+ # by [{,] (apparently space
+ # needed for IE6)
+ lambda g: nl_unesc_sub('', g(18)), # nl_string
+ lambda g: post_esc_sub(' ', g(19)), # escape
+ )
+
+ def func(match):
+ """ Main subber """
+ idx, group = match.lastindex, match.group
+ if idx > 3:
+ return table[idx](group)
+
+ # shortcuts for frequent operations below:
+ elif idx == 1: # not interesting
+ return group(1)
+ #else: # space with token before or at the beginning
+ return space_sub(space_subber, group(idx))
+
+ return func
+
+ def cssmin(style, keep_bang_comments=False): # pylint: disable = W0621
+ """
+ Minify CSS.
+
+ :Parameters:
+ `style` : ``str``
+ CSS to minify
+
+ `keep_bang_comments` : ``bool``
+ Keep comments starting with an exclamation mark? (``/*!...*/``)
+
+ :Return: Minified style
+ :Rtype: ``str``
+ """
+ return main_sub(main_subber(keep_bang_comments), style)
+
+ return cssmin
+
+cssmin = _make_cssmin()
+
+
+if __name__ == '__main__':
+ def main():
+ """ Main """
+ import sys as _sys
+ keep_bang_comments = (
+ '-b' in _sys.argv[1:]
+ or '-bp' in _sys.argv[1:]
+ or '-pb' in _sys.argv[1:]
+ )
+ if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \
+ or '-pb' in _sys.argv[1:]:
+ global cssmin # pylint: disable = W0603
+ cssmin = _make_cssmin(python_only=True)
+ _sys.stdout.write(cssmin(
+ _sys.stdin.read(), keep_bang_comments=keep_bang_comments
+ ))
+ main()
diff --git a/venv/lib/python2.7/site-packages/ckan/include/rjsmin.py b/venv/lib/python2.7/site-packages/ckan/include/rjsmin.py
new file mode 100644
index 00000000..13e66e0e
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/include/rjsmin.py
@@ -0,0 +1,290 @@
+#!/usr/bin/env python
+# -*- coding: ascii -*-
+#
+# Copyright 2011, 2012
+# Andr\xe9 Malo or his licensors, as applicable
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+r"""
+=====================
+ Javascript Minifier
+=====================
+
+rJSmin is a javascript minifier written in python.
+
+The minifier is based on the semantics of `jsmin.c by Douglas Crockford`_\.
+
+The module is a re-implementation aiming for speed, so it can be used at
+runtime (rather than during a preprocessing step). Usually it produces the
+same results as the original ``jsmin.c``. It differs in the following ways:
+
+- there is no error detection: unterminated string, regex and comment
+ literals are treated as regular javascript code and minified as such.
+- Control characters inside string and regex literals are left untouched; they
+ are not converted to spaces (nor to \n)
+- Newline characters are not allowed inside string and regex literals, except
+ for line continuations in string literals (ECMA-5).
+- "return /regex/" is recognized correctly.
+- "+ ++" and "- --" sequences are not collapsed to '+++' or '---'
+- rJSmin does not handle streams, but only complete strings. (However, the
+ module provides a "streamy" interface).
+
+Since most parts of the logic are handled by the regex engine it's way
+faster than the original python port of ``jsmin.c`` by Baruch Even. The speed
+factor varies between about 6 and 55 depending on input and python version
+(it gets faster the more compressed the input already is). Compared to the
+speed-refactored python port by Dave St.Germain the performance gain is less
+dramatic but still between 1.2 and 7. See the docs/BENCHMARKS file for
+details.
+
+rjsmin.c is a reimplementation of rjsmin.py in C and speeds it up even more.
+
+Both python 2 and python 3 are supported.
+
+.. _jsmin.c by Douglas Crockford:
+ http://www.crockford.com/javascript/jsmin.c
+"""
+__author__ = "Andr\xe9 Malo"
+__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')
+__docformat__ = "restructuredtext en"
+__license__ = "Apache License, Version 2.0"
+__version__ = '1.0.3'
+__all__ = ['jsmin']
+
+import re as _re
+
+
+def _make_jsmin(python_only=False):
+ """
+ Generate JS minifier based on `jsmin.c by Douglas Crockford`_
+
+ .. _jsmin.c by Douglas Crockford:
+ http://www.crockford.com/javascript/jsmin.c
+
+ :Parameters:
+ `python_only` : ``bool``
+ Use only the python variant. If true, the c extension is not even
+ tried to be loaded.
+
+ :Return: Minifier
+ :Rtype: ``callable``
+ """
+ # pylint: disable = R0912, R0914, W0612
+ if not python_only:
+ try:
+ import _rjsmin
+ except ImportError:
+ pass
+ else:
+ return _rjsmin.jsmin
+ try:
+ xrange
+ except NameError:
+ xrange = range # pylint: disable = W0622
+
+ space_chars = r'[\000-\011\013\014\016-\040]'
+
+ line_comment = r'(?://[^\r\n]*)'
+ space_comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
+ string1 = \
+ r'(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^\047\\\r\n]*)*\047)'
+ string2 = r'(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^"\\\r\n]*)*")'
+ strings = r'(?:%s|%s)' % (string1, string2)
+
+ charclass = r'(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\])'
+ nospecial = r'[^/\\\[\r\n]'
+ regex = r'(?:/(?![\r\n/*])%s*(?:(?:\\[^\r\n]|%s)%s*)*/)' % (
+ nospecial, charclass, nospecial
+ )
+ space = r'(?:%s|%s)' % (space_chars, space_comment)
+ newline = r'(?:%s?[\r\n])' % line_comment
+
+ def fix_charclass(result):
+ """ Fixup string of chars to fit into a regex char class """
+ pos = result.find('-')
+ if pos >= 0:
+ result = r'%s%s-' % (result[:pos], result[pos + 1:])
+
+ def sequentize(string):
+ """
+ Notate consecutive characters as sequence
+
+ (1-4 instead of 1234)
+ """
+ first, last, result = None, None, []
+ for char in map(ord, string):
+ if last is None:
+ first = last = char
+ elif last + 1 == char:
+ last = char
+ else:
+ result.append((first, last))
+ first = last = char
+ if last is not None:
+ result.append((first, last))
+ return ''.join(['%s%s%s' % (
+ chr(first),
+ last > first + 1 and '-' or '',
+ last != first and chr(last) or ''
+ ) for first, last in result])
+
+ return _re.sub(r'([\000-\040\047])', # for better portability
+ lambda m: '\\%03o' % ord(m.group(1)), (sequentize(result)
+ .replace('\\', '\\\\')
+ .replace('[', '\\[')
+ .replace(']', '\\]')
+ )
+ )
+
+ def id_literal_(what):
+ """ Make id_literal like char class """
+ match = _re.compile(what).match
+ result = ''.join([
+ chr(c) for c in xrange(127) if not match(chr(c))
+ ])
+ return '[^%s]' % fix_charclass(result)
+
+ def not_id_literal_(keep):
+ """ Make negated id_literal like char class """
+ match = _re.compile(id_literal_(keep)).match
+ result = ''.join([
+ chr(c) for c in xrange(127) if not match(chr(c))
+ ])
+ return r'[%s]' % fix_charclass(result)
+
+ not_id_literal = not_id_literal_(r'[a-zA-Z0-9_$]')
+ preregex1 = r'[(,=:\[!&|?{};\r\n]'
+ preregex2 = r'%(not_id_literal)sreturn' % locals()
+
+ id_literal = id_literal_(r'[a-zA-Z0-9_$]')
+ id_literal_open = id_literal_(r'[a-zA-Z0-9_${\[(+-]')
+ id_literal_close = id_literal_(r'[a-zA-Z0-9_$}\])"\047+-]')
+
+ space_sub = _re.compile((
+ r'([^\047"/\000-\040]+)'
+ r'|(%(strings)s[^\047"/\000-\040]*)'
+ r'|(?:(?<=%(preregex1)s)%(space)s*(%(regex)s[^\047"/\000-\040]*))'
+ r'|(?:(?<=%(preregex2)s)%(space)s*(%(regex)s[^\047"/\000-\040]*))'
+ r'|(?<=%(id_literal_close)s)'
+ r'%(space)s*(?:(%(newline)s)%(space)s*)+'
+ r'(?=%(id_literal_open)s)'
+ r'|(?<=%(id_literal)s)(%(space)s)+(?=%(id_literal)s)'
+ r'|(?<=\+)(%(space)s)+(?=\+\+)'
+ r'|(?<=-)(%(space)s)+(?=--)'
+ r'|%(space)s+'
+ r'|(?:%(newline)s%(space)s*)+'
+ ) % locals()).sub
+ #print space_sub.__self__.pattern
+
+ def space_subber(match):
+ """ Substitution callback """
+ # pylint: disable = C0321, R0911
+ groups = match.groups()
+ if groups[0]: return groups[0]
+ elif groups[1]: return groups[1]
+ elif groups[2]: return groups[2]
+ elif groups[3]: return groups[3]
+ elif groups[4]: return '\n'
+ elif groups[5] or groups[6] or groups[7]: return ' '
+ else: return ''
+
+ def jsmin(script): # pylint: disable = W0621
+ r"""
+ Minify javascript based on `jsmin.c by Douglas Crockford`_\.
+
+ Instead of parsing the stream char by char, it uses a regular
+ expression approach which minifies the whole script with one big
+ substitution regex.
+
+ .. _jsmin.c by Douglas Crockford:
+ http://www.crockford.com/javascript/jsmin.c
+
+ :Parameters:
+ `script` : ``str``
+ Script to minify
+
+ :Return: Minified script
+ :Rtype: ``str``
+ """
+ return space_sub(space_subber, '\n%s\n' % script).strip()
+
+ return jsmin
+
+jsmin = _make_jsmin()
+
+
+def jsmin_for_posers(script):
+ r"""
+ Minify javascript based on `jsmin.c by Douglas Crockford`_\.
+
+ Instead of parsing the stream char by char, it uses a regular
+ expression approach which minifies the whole script with one big
+ substitution regex.
+
+ .. _jsmin.c by Douglas Crockford:
+ http://www.crockford.com/javascript/jsmin.c
+
+ :Warning: This function is the digest of a _make_jsmin() call. It just
+ utilizes the resulting regex. It's just for fun here and may
+ vanish any time. Use the `jsmin` function instead.
+
+ :Parameters:
+ `script` : ``str``
+ Script to minify
+
+ :Return: Minified script
+ :Rtype: ``str``
+ """
+ def subber(match):
+ """ Substitution callback """
+ groups = match.groups()
+ return (
+ groups[0] or
+ groups[1] or
+ groups[2] or
+ groups[3] or
+ (groups[4] and '\n') or
+ (groups[5] and ' ') or
+ (groups[6] and ' ') or
+ (groups[7] and ' ') or
+ ''
+ )
+
+ return _re.sub(
+ r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
+ r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
+ r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
+ r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
+ r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
+ r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
+ r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
+ r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
+ r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
+ r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
+ r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
+ r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
+ r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-#%-\04'
+ r'7)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-\011'
+ r'\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^\000-'
+ r'#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|(?:/'
+ r'\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+\+)|(?<=-)((?:[\000-\011\013'
+ r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=--)|(?:[\00'
+ r'0-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:('
+ r'?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]'
+ r'*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
+ ).strip()
+
+
+if __name__ == '__main__':
+ import sys as _sys
+ _sys.stdout.write(jsmin(_sys.stdin.read()))
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/__init__.py b/venv/lib/python2.7/site-packages/ckan/lib/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/activity_streams.py b/venv/lib/python2.7/site-packages/ckan/lib/activity_streams.py
new file mode 100644
index 00000000..70c17730
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/activity_streams.py
@@ -0,0 +1,262 @@
+# encoding: utf-8
+
+import re
+
+from webhelpers.html import literal
+
+import ckan.lib.helpers as h
+import ckan.lib.base as base
+import ckan.logic as logic
+
+from ckan.common import _, is_flask_request
+
+# get_snippet_*() functions replace placeholders like {user}, {dataset}, etc.
+# in activity strings with HTML representations of particular users, datasets,
+# etc.
+
+def get_snippet_actor(activity, detail):
+ return literal('''%s '''
+ % (h.linked_user(activity['user_id'], 0, 30))
+ )
+
+def get_snippet_user(activity, detail):
+ return literal('''%s '''
+ % (h.linked_user(activity['object_id'], 0, 20))
+ )
+
+def get_snippet_dataset(activity, detail):
+ data = activity['data']
+ pkg_dict = data.get('package') or data.get('dataset')
+ link = h.dataset_link(pkg_dict) if pkg_dict else ''
+ return literal('''%s '''
+ % (link)
+ )
+
+def get_snippet_tag(activity, detail):
+ return h.tag_link(detail['data']['tag'])
+
+def get_snippet_group(activity, detail):
+ link = h.group_link(activity['data']['group'])
+ return literal('''%s '''
+ % (link)
+ )
+
+def get_snippet_organization(activity, detail):
+ return h.organization_link(activity['data']['group'])
+
+def get_snippet_extra(activity, detail):
+ return '"%s"' % detail['data']['package_extra']['key']
+
+def get_snippet_resource(activity, detail):
+ return h.resource_link(detail['data']['resource'],
+ activity['data']['package']['id'])
+
+# activity_stream_string_*() functions return translatable string
+# representations of activity types, the strings contain placeholders like
+# {user}, {dataset} etc. to be replaced with snippets from the get_snippet_*()
+# functions above.
+
+def activity_stream_string_added_tag(context, activity):
+ return _("{actor} added the tag {tag} to the dataset {dataset}")
+
+def activity_stream_string_changed_group(context, activity):
+ return _("{actor} updated the group {group}")
+
+def activity_stream_string_changed_organization(context, activity):
+ return _("{actor} updated the organization {organization}")
+
+def activity_stream_string_changed_package(context, activity):
+ return _("{actor} updated the dataset {dataset}")
+
+def activity_stream_string_changed_package_extra(context, activity):
+ return _("{actor} changed the extra {extra} of the dataset {dataset}")
+
+def activity_stream_string_changed_resource(context, activity):
+ return _("{actor} updated the resource {resource} in the dataset {dataset}")
+
+def activity_stream_string_changed_user(context, activity):
+ return _("{actor} updated their profile")
+
+def activity_stream_string_deleted_group(context, activity):
+ return _("{actor} deleted the group {group}")
+
+def activity_stream_string_deleted_organization(context, activity):
+ return _("{actor} deleted the organization {organization}")
+
+def activity_stream_string_deleted_package(context, activity):
+ return _("{actor} deleted the dataset {dataset}")
+
+def activity_stream_string_deleted_package_extra(context, activity):
+ return _("{actor} deleted the extra {extra} from the dataset {dataset}")
+
+def activity_stream_string_deleted_resource(context, activity):
+ return _("{actor} deleted the resource {resource} from the dataset "
+ "{dataset}")
+
+def activity_stream_string_new_group(context, activity):
+ return _("{actor} created the group {group}")
+
+def activity_stream_string_new_organization(context, activity):
+ return _("{actor} created the organization {organization}")
+
+def activity_stream_string_new_package(context, activity):
+ return _("{actor} created the dataset {dataset}")
+
+def activity_stream_string_new_package_extra(context, activity):
+ return _("{actor} added the extra {extra} to the dataset {dataset}")
+
+def activity_stream_string_new_resource(context, activity):
+ return _("{actor} added the resource {resource} to the dataset {dataset}")
+
+def activity_stream_string_new_user(context, activity):
+ return _("{actor} signed up")
+
+def activity_stream_string_removed_tag(context, activity):
+ return _("{actor} removed the tag {tag} from the dataset {dataset}")
+
+def activity_stream_string_follow_dataset(context, activity):
+ return _("{actor} started following {dataset}")
+
+def activity_stream_string_follow_user(context, activity):
+ return _("{actor} started following {user}")
+
+def activity_stream_string_follow_group(context, activity):
+ return _("{actor} started following {group}")
+
+# A dictionary mapping activity snippets to functions that expand the snippets.
+activity_snippet_functions = {
+ 'actor': get_snippet_actor,
+ 'user': get_snippet_user,
+ 'dataset': get_snippet_dataset,
+ 'tag': get_snippet_tag,
+ 'group': get_snippet_group,
+ 'organization': get_snippet_organization,
+ 'extra': get_snippet_extra,
+ 'resource': get_snippet_resource,
+}
+
+# A dictionary mapping activity types to functions that return translatable
+# string descriptions of the activity types.
+activity_stream_string_functions = {
+ 'added tag': activity_stream_string_added_tag,
+ 'changed group': activity_stream_string_changed_group,
+ 'changed organization': activity_stream_string_changed_organization,
+ 'changed package': activity_stream_string_changed_package,
+ 'changed package_extra': activity_stream_string_changed_package_extra,
+ 'changed resource': activity_stream_string_changed_resource,
+ 'changed user': activity_stream_string_changed_user,
+ 'deleted group': activity_stream_string_deleted_group,
+ 'deleted organization': activity_stream_string_deleted_organization,
+ 'deleted package': activity_stream_string_deleted_package,
+ 'deleted package_extra': activity_stream_string_deleted_package_extra,
+ 'deleted resource': activity_stream_string_deleted_resource,
+ 'new group': activity_stream_string_new_group,
+ 'new organization': activity_stream_string_new_organization,
+ 'new package': activity_stream_string_new_package,
+ 'new package_extra': activity_stream_string_new_package_extra,
+ 'new resource': activity_stream_string_new_resource,
+ 'new user': activity_stream_string_new_user,
+ 'removed tag': activity_stream_string_removed_tag,
+ 'follow dataset': activity_stream_string_follow_dataset,
+ 'follow user': activity_stream_string_follow_user,
+ 'follow group': activity_stream_string_follow_group,
+}
+
+# A dictionary mapping activity types to the icons associated to them
+activity_stream_string_icons = {
+ 'added tag': 'tag',
+ 'changed group': 'users',
+ 'changed package': 'sitemap',
+ 'changed package_extra': 'pencil-square-o',
+ 'changed resource': 'file',
+ 'changed user': 'user',
+ 'deleted group': 'users',
+ 'deleted package': 'sitemap',
+ 'deleted package_extra': 'pencil-square-o',
+ 'deleted resource': 'file',
+ 'new group': 'users',
+ 'new package': 'sitemap',
+ 'new package_extra': 'pencil-square-o',
+ 'new resource': 'file',
+ 'new user': 'user',
+ 'removed tag': 'tag',
+ 'follow dataset': 'sitemap',
+ 'follow user': 'user',
+ 'follow group': 'users',
+ 'changed organization': 'briefcase',
+ 'deleted organization': 'briefcase',
+ 'new organization': 'briefcase',
+ 'undefined': 'certificate', # This is when no activity icon can be found
+}
+
+# A list of activity types that may have details
+activity_stream_actions_with_detail = ['changed package']
+
+def activity_list_to_html(context, activity_stream, extra_vars):
+ '''Return the given activity stream as a snippet of HTML.
+
+ :param activity_stream: the activity stream to render
+ :type activity_stream: list of activity dictionaries
+ :param extra_vars: extra variables to pass to the activity stream items
+ template when rendering it
+ :type extra_vars: dictionary
+
+ :rtype: HTML-formatted string
+
+ '''
+ activity_list = [] # These are the activity stream messages.
+ for activity in activity_stream:
+ detail = None
+ activity_type = activity['activity_type']
+ # Some activity types may have details.
+ if activity_type in activity_stream_actions_with_detail:
+ details = logic.get_action('activity_detail_list')(context=context,
+ data_dict={'id': activity['id']})
+ # If an activity has just one activity detail then render the
+ # detail instead of the activity.
+ if len(details) == 1:
+ detail = details[0]
+ object_type = detail['object_type']
+
+ if object_type == 'PackageExtra':
+ object_type = 'package_extra'
+
+ new_activity_type = '%s %s' % (detail['activity_type'],
+ object_type.lower())
+ if new_activity_type in activity_stream_string_functions:
+ activity_type = new_activity_type
+
+ if not activity_type in activity_stream_string_functions:
+ raise NotImplementedError("No activity renderer for activity "
+ "type '%s'" % activity_type)
+
+ if activity_type in activity_stream_string_icons:
+ activity_icon = activity_stream_string_icons[activity_type]
+ else:
+ activity_icon = activity_stream_string_icons['undefined']
+
+ activity_msg = activity_stream_string_functions[activity_type](context,
+ activity)
+
+ # Get the data needed to render the message.
+ matches = re.findall('\{([^}]*)\}', activity_msg)
+ data = {}
+ for match in matches:
+ snippet = activity_snippet_functions[match](activity, detail)
+ data[str(match)] = snippet
+
+ activity_list.append({'msg': activity_msg,
+ 'type': activity_type.replace(' ', '-').lower(),
+ 'icon': activity_icon,
+ 'data': data,
+ 'timestamp': activity['timestamp'],
+ 'is_new': activity.get('is_new', False)})
+ extra_vars['activities'] = activity_list
+
+ # TODO: Do this properly without having to check if it's Flask or not
+ if is_flask_request():
+ return base.render('activity_streams/activity_stream_items.html',
+ extra_vars=extra_vars)
+ else:
+ return literal(base.render('activity_streams/activity_stream_items.html',
+ extra_vars=extra_vars))
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/activity_streams_session_extension.py b/venv/lib/python2.7/site-packages/ckan/lib/activity_streams_session_extension.py
new file mode 100644
index 00000000..85cf677f
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/activity_streams_session_extension.py
@@ -0,0 +1,153 @@
+# encoding: utf-8
+
+from ckan.common import config
+from sqlalchemy.orm.session import SessionExtension
+from paste.deploy.converters import asbool
+import logging
+
+log = logging.getLogger(__name__)
+
+
+def activity_stream_item(obj, activity_type, revision, user_id):
+ method = getattr(obj, "activity_stream_item", None)
+ if callable(method):
+ return method(activity_type, revision, user_id)
+ else:
+ # Object did not have a suitable activity_stream_item() method; it must
+ # not be a package
+ return None
+
+
+def activity_stream_detail(obj, activity_id, activity_type):
+ method = getattr(obj, "activity_stream_detail", None)
+ if callable(method):
+ return method(activity_id, activity_type)
+ else:
+ # Object did not have a suitable activity_stream_detail() method
+ return None
+
+
+class DatasetActivitySessionExtension(SessionExtension):
+ """Session extension that emits activity stream activities for packages
+ and related objects.
+
+ An SQLAlchemy SessionExtension that watches for new, changed or deleted
+ Packages or objects with related packages (Resources, PackageExtras..)
+ being committed to the SQLAlchemy session and creates Activity and
+ ActivityDetail objects for these activities.
+
+ For most types of activity the Activity and ActivityDetail objects are
+ created in the relevant ckan/logic/action/ functions, but for Packages and
+ objects with related packages they are created by this class instead.
+
+ """
+ def before_commit(self, session):
+ if not asbool(config.get('ckan.activity_streams_enabled', 'true')):
+ return
+
+ session.flush()
+
+ try:
+ object_cache = session._object_cache
+ revision = session.revision
+ except AttributeError:
+ # session had no _object_cache or no revision; skipping this commit
+ return
+
+ if revision.user:
+ user_id = revision.user.id
+ else:
+ # If the user is not logged in then revision.user is None and
+ # revision.author is their IP address. Just log them as 'not logged
+ # in' rather than logging their IP address.
+ user_id = 'not logged in'
+
+ # The top-level objects that we will append to the activity table. The
+ # keys here are package IDs, and the values are model.activity:Activity
+ # objects.
+ activities = {}
+
+ # The second-level objects that we will append to the activity_detail
+ # table. Each row in the activity table has zero or more related rows
+ # in the activity_detail table. The keys here are activity IDs, and the
+ # values are lists of model.activity:ActivityDetail objects.
+ activity_details = {}
+
+ # Log new packages first to prevent them from getting incorrectly
+ # logged as changed packages.
+ # Looking for new packages...
+ for obj in object_cache['new']:
+ activity = activity_stream_item(obj, 'new', revision, user_id)
+ if activity is None:
+ continue
+ # The object returns an activity stream item, so we know that the
+ # object is a package.
+
+ # Don't create activities for private datasets.
+ if obj.private:
+ continue
+
+ activities[obj.id] = activity
+
+ activity_detail = activity_stream_detail(obj, activity.id, "new")
+ if activity_detail is not None:
+ activity_details[activity.id] = [activity_detail]
+
+ # Now process other objects.
+ for activity_type in ('new', 'changed', 'deleted'):
+ objects = object_cache[activity_type]
+ for obj in objects:
+
+ if not hasattr(obj, "id"):
+ # Object has no id; skipping
+ continue
+
+ if (activity_type in ('new', 'changed') and
+ obj.id in activities):
+ # This object was already logged as a new package
+ continue
+
+ try:
+ related_packages = obj.related_packages()
+ except (AttributeError, TypeError):
+ # Object did not have a suitable related_packages() method;
+ # skipping it
+ continue
+
+ for package in related_packages:
+ if package is None:
+ continue
+
+ # Don't create activities for private datasets.
+ if package.private:
+ continue
+
+ if package.id in activities:
+ activity = activities[package.id]
+ else:
+ activity = activity_stream_item(
+ package, "changed", revision, user_id)
+ if activity is None:
+ continue
+
+ activity_detail = activity_stream_detail(
+ obj, activity.id, activity_type)
+ if activity_detail is not None:
+ if not package.id in activities:
+ activities[package.id] = activity
+ if activity_details.has_key(activity.id):
+ activity_details[activity.id].append(
+ activity_detail)
+ else:
+ activity_details[activity.id] = [activity_detail]
+
+ for key, activity in activities.items():
+ # Emitting activity
+ session.add(activity)
+
+ for key, activity_detail_list in activity_details.items():
+ for activity_detail_obj in activity_detail_list:
+ # Emitting activity detail
+ session.add(activity_detail_obj)
+
+ session.flush()
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/alphabet_paginate.py b/venv/lib/python2.7/site-packages/ckan/lib/alphabet_paginate.py
new file mode 100644
index 00000000..b04b7aa4
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/alphabet_paginate.py
@@ -0,0 +1,159 @@
+# encoding: utf-8
+
+'''
+Based on webhelpers.paginator, but:
+ * each page is for items beginning with a particular letter
+ * output is suitable for Bootstrap
+
+ Example:
+ c.page = h.Page(
+ collection=query,
+ page=request.params.get('page', 'A'),
+ )
+ Template:
+ ${c.page.pager()}
+ ${package_list(c.page.items)}
+ ${c.page.pager()}
+'''
+from itertools import dropwhile
+import re
+
+from six import text_type
+from sqlalchemy import __version__ as sqav
+from sqlalchemy.orm.query import Query
+from webhelpers.html.builder import HTML
+from ckan.lib.helpers import url_for
+
+
+class AlphaPage(object):
+ def __init__(self, collection, alpha_attribute, page, other_text, paging_threshold=50,
+ controller_name='tag'):
+ '''
+ @param collection - sqlalchemy query of all the items to paginate
+ @param alpha_attribute - name of the attribute (on each item of the
+ collection) which has the string to paginate by
+ @param page - the page identifier - the start character or other_text
+ @param other_text - the (i18n-ized) string for items with
+ non-alphabetic first character.
+ @param paging_threshold - the minimum number of items required to
+ start paginating them.
+ @param controller_name - The name of the controller that will be linked to,
+ which defaults to tag. The controller name should be the
+ same as the route so for some this will be the full
+ controller name such as 'A.B.controllers.C:ClassName'
+ '''
+ self.collection = collection
+ self.alpha_attribute = alpha_attribute
+ self.page = page
+ self.other_text = other_text
+ self.paging_threshold = paging_threshold
+ self.controller_name = controller_name
+
+ self.letters = [char for char in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + [self.other_text]
+
+ # Work out which alphabet letters are 'available' i.e. have some results
+ # because we grey-out those which aren't.
+ self.available = dict( (c,0,) for c in self.letters )
+ for c in self.collection:
+ if isinstance(c, text_type):
+ x = c[0]
+ elif isinstance(c, dict):
+ x = c[self.alpha_attribute][0]
+ else:
+ x = getattr(c, self.alpha_attribute)[0]
+ x = x.upper()
+ if x not in self.letters:
+ x = self.other_text
+ self.available[x] = self.available.get(x, 0) + 1
+
+ def pager(self, q=None):
+ '''Returns pager html - for navigating between the pages.
+ e.g. Something like this:
+
+ '''
+ if self.item_count < self.paging_threshold:
+ return ''
+ pages = []
+ page = q or self.page
+ for letter in self.letters:
+ href = url_for(controller=self.controller_name, action='index', page=letter)
+ link = HTML.a(href=href, c=letter)
+ if letter != page:
+ if self.available.get(letter, 0):
+ li_class = ''
+ else:
+ li_class = 'disabled'
+ else:
+ li_class = 'active'
+ attributes = {'class_': li_class} if li_class else {}
+ page_element = HTML.li(link, **attributes)
+ pages.append(page_element)
+ ul = HTML.tag('ul', *pages)
+ div = HTML.div(ul, class_='pagination pagination-alphabet')
+ return div
+
+
+ @property
+ def items(self):
+ '''Returns items on the current page.'''
+ if isinstance(self.collection, Query):
+ query = self.collection
+ if sqav.startswith("0.4"):
+ attribute = getattr(query.table.c,
+ self.alpha_attribute)
+ elif sqav.startswith("0.5"):
+ attribute = getattr(query._entity_zero().selectable.c,
+ self.alpha_attribute)
+ else:
+ entity = getattr(query.column_descriptions[0]['expr'],
+ self.alpha_attribute)
+ query = query.add_columns(entity)
+ column = dropwhile(lambda x: x['name'] != \
+ self.alpha_attribute,
+ query.column_descriptions)
+ attribute = column.next()['expr']
+ if self.item_count >= self.paging_threshold:
+ if self.page != self.other_text:
+ query = query.filter(attribute.ilike(u'%s%%' % self.page))
+ else:
+ # regexp search
+ query = query.filter(attribute.op('~')(u'^[^a-zA-Z].*'))
+ query.order_by(attribute)
+ return query.all()
+ elif isinstance(self.collection,list):
+ if self.item_count >= self.paging_threshold:
+ if self.page != self.other_text:
+ if isinstance(self.collection[0], dict):
+ items = [x for x in self.collection if x[self.alpha_attribute][0:1].lower() == self.page.lower()]
+ elif isinstance(self.collection[0], text_type):
+ items = [x for x in self.collection if x[0:1].lower() == self.page.lower()]
+ else:
+ items = [x for x in self.collection if getattr(x,self.alpha_attribute)[0:1].lower() == self.page.lower()]
+ else:
+ # regexp search
+ if isinstance(self.collection[0], dict):
+ items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x[self.alpha_attribute])]
+ else:
+ items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x)]
+ items.sort()
+ else:
+ items = self.collection
+ return items
+ else:
+ raise NotImplementedError
+
+ @property
+ def item_count(self):
+ if isinstance(self.collection, Query):
+ return self.collection.count()
+ elif isinstance(self.collection,list):
+ return len(self.collection)
+ else:
+ raise NotImplementedError
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/app_globals.py b/venv/lib/python2.7/site-packages/ckan/lib/app_globals.py
new file mode 100644
index 00000000..0b7a5889
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/app_globals.py
@@ -0,0 +1,225 @@
+# encoding: utf-8
+
+''' The application's Globals object '''
+
+import logging
+import time
+from threading import Lock
+import re
+
+from paste.deploy.converters import asbool
+from ckan.common import config
+
+import ckan
+import ckan.model as model
+import ckan.logic as logic
+from logic.schema import update_configuration_schema
+
+
+log = logging.getLogger(__name__)
+
+
+# mappings translate between config settings and globals because our naming
+# conventions are not well defined and/or implemented
+mappings = {
+# 'config_key': 'globals_key',
+}
+
+
+# This mapping is only used to define the configuration options (from the
+# `config` object) that should be copied to the `app_globals` (`g`) object.
+app_globals_from_config_details = {
+ 'ckan.site_title': {},
+ 'ckan.site_logo': {},
+ 'ckan.site_url': {},
+ 'ckan.site_description': {},
+ 'ckan.site_about': {},
+ 'ckan.site_intro_text': {},
+ 'ckan.site_custom_css': {},
+ 'ckan.favicon': {}, # default gets set in config.environment.py
+ 'ckan.template_head_end': {},
+ 'ckan.template_footer_end': {},
+ # has been setup in load_environment():
+ 'ckan.site_id': {},
+ 'ckan.recaptcha.publickey': {'name': 'recaptcha_publickey'},
+ 'ckan.template_title_deliminater': {'default': '-'},
+ 'ckan.template_head_end': {},
+ 'ckan.template_footer_end': {},
+ 'ckan.dumps_url': {},
+ 'ckan.dumps_format': {},
+ 'ofs.impl': {'name': 'ofs_impl'},
+ 'ckan.homepage_style': {'default': '1'},
+
+ # split string
+ 'search.facets': {'default': 'organization groups tags res_format license_id',
+ 'type': 'split',
+ 'name': 'facets'},
+ 'package_hide_extras': {'type': 'split'},
+ 'ckan.plugins': {'type': 'split'},
+
+ # bool
+ 'debug': {'default': 'false', 'type' : 'bool'},
+ 'ckan.debug_supress_header' : {'default': 'false', 'type' : 'bool'},
+ 'ckan.legacy_templates' : {'default': 'false', 'type' : 'bool'},
+ 'ckan.tracking_enabled' : {'default': 'false', 'type' : 'bool'},
+
+ # int
+ 'ckan.datasets_per_page': {'default': '20', 'type': 'int'},
+ 'ckan.activity_list_limit': {'default': '30', 'type': 'int'},
+ 'ckan.user_list_limit': {'default': '20', 'type': 'int'},
+ 'search.facets.default': {'default': '10', 'type': 'int',
+ 'name': 'facets_default_number'},
+}
+
+
+# A place to store the origional config options of we override them
+_CONFIG_CACHE = {}
+
+def set_main_css(css_file):
+ ''' Sets the main_css. The css_file must be of the form file.css '''
+ assert css_file.endswith('.css')
+ new_css = css_file
+ # FIXME we should check the css file exists
+ app_globals.main_css = str(new_css)
+
+
+def set_app_global(key, value):
+ '''
+ Set a new key on the app_globals (g) object
+
+ It will process the value according to the options on
+ app_globals_from_config_details (if any)
+ '''
+ key, value = process_app_global(key, value)
+ setattr(app_globals, key, value)
+
+
+def process_app_global(key, value):
+ '''
+ Tweak a key, value pair meant to be set on the app_globals (g) object
+
+ According to the options on app_globals_from_config_details (if any)
+ '''
+ options = app_globals_from_config_details.get(key)
+ key = get_globals_key(key)
+ if options:
+ if 'name' in options:
+ key = options['name']
+ value = value or options.get('default', '')
+
+ data_type = options.get('type')
+ if data_type == 'bool':
+ value = asbool(value)
+ elif data_type == 'int':
+ value = int(value)
+ elif data_type == 'split':
+ value = value.split()
+
+ return key, value
+
+
+def get_globals_key(key):
+ # create our globals key
+ # these can be specified in mappings or else we remove
+ # the `ckan.` part this is to keep the existing namings
+ # set the value
+ if key in mappings:
+ return mappings[key]
+ elif key.startswith('ckan.'):
+ return key[5:]
+ else:
+ return key
+
+
+def reset():
+ ''' set updatable values from config '''
+ def get_config_value(key, default=''):
+ if model.meta.engine.has_table('system_info'):
+ value = model.get_system_info(key)
+ else:
+ value = None
+ config_value = config.get(key)
+ # sort encodeings if needed
+ if isinstance(config_value, str):
+ try:
+ config_value = config_value.decode('utf-8')
+ except UnicodeDecodeError:
+ config_value = config_value.decode('latin-1')
+ # we want to store the config the first time we get here so we can
+ # reset them if needed
+ if key not in _CONFIG_CACHE:
+ _CONFIG_CACHE[key] = config_value
+ if value is not None:
+ log.debug('config `%s` set to `%s` from db' % (key, value))
+ else:
+ value = _CONFIG_CACHE[key]
+ if value:
+ log.debug('config `%s` set to `%s` from config' % (key, value))
+ else:
+ value = default
+
+ set_app_global(key, value)
+
+ # update the config
+ config[key] = value
+
+ return value
+
+ # update the config settings in auto update
+ schema = update_configuration_schema()
+ for key in schema.keys():
+ get_config_value(key)
+
+ # custom styling
+ main_css = get_config_value('ckan.main_css', '/base/css/main.css')
+ set_main_css(main_css)
+
+ if app_globals.site_logo:
+ app_globals.header_class = 'header-image'
+ elif not app_globals.site_description:
+ app_globals.header_class = 'header-text-logo'
+ else:
+ app_globals.header_class = 'header-text-logo-tagline'
+
+
+class _Globals(object):
+
+ ''' Globals acts as a container for objects available throughout the
+ life of the application. '''
+
+ def __init__(self):
+ '''One instance of Globals is created during application
+ initialization and is available during requests via the
+ 'app_globals' variable
+ '''
+ self._init()
+ self._config_update = None
+ self._mutex = Lock()
+
+ def _check_uptodate(self):
+ ''' check the config is uptodate needed when several instances are
+ running '''
+ value = model.get_system_info('ckan.config_update')
+ if self._config_update != value:
+ if self._mutex.acquire(False):
+ reset()
+ self._config_update = value
+ self._mutex.release()
+
+ def _init(self):
+
+ self.ckan_version = ckan.__version__
+ self.ckan_base_version = re.sub('[^0-9\.]', '', self.ckan_version)
+ if self.ckan_base_version == self.ckan_version:
+ self.ckan_doc_version = self.ckan_version[:3]
+ else:
+ self.ckan_doc_version = 'latest'
+
+ # process the config details to set globals
+ for key in app_globals_from_config_details.keys():
+ new_key, value = process_app_global(key, config.get(key) or '')
+ setattr(self, new_key, value)
+
+
+app_globals = _Globals()
+del _Globals
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/auth_tkt.py b/venv/lib/python2.7/site-packages/ckan/lib/auth_tkt.py
new file mode 100644
index 00000000..36faea56
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/auth_tkt.py
@@ -0,0 +1,87 @@
+# encoding: utf-8
+
+import logging
+import math
+import os
+
+from ckan.common import config
+from repoze.who.plugins import auth_tkt as repoze_auth_tkt
+
+_bool = repoze_auth_tkt._bool
+
+log = logging.getLogger(__name__)
+
+
+class CkanAuthTktCookiePlugin(repoze_auth_tkt.AuthTktCookiePlugin):
+
+ def __init__(self, httponly, *args, **kwargs):
+ super(CkanAuthTktCookiePlugin, self).__init__(*args, **kwargs)
+ self.httponly = httponly
+
+ def _get_cookies(self, *args, **kwargs):
+ '''
+ Override method in superclass to ensure HttpOnly is set appropriately.
+ '''
+ super_cookies = super(CkanAuthTktCookiePlugin, self). \
+ _get_cookies(*args, **kwargs)
+
+ cookies = []
+ for k, v in super_cookies:
+ replace_with = '; HttpOnly' if self.httponly else ''
+ v = v.replace('; HttpOnly', '') + replace_with
+ cookies.append((k, v))
+
+ return cookies
+
+
+def make_plugin(secret=None,
+ secretfile=None,
+ cookie_name='auth_tkt',
+ secure=False,
+ include_ip=False,
+ timeout=None,
+ reissue_time=None,
+ userid_checker=None):
+ from repoze.who.utils import resolveDotted
+
+ # ckan specifics:
+ # Get secret from beaker setting if necessary
+ if secret is None or secret == 'somesecret':
+ secret = config['beaker.session.secret']
+ # Session timeout and reissue time for auth cookie
+ if timeout is None and config.get('who.timeout'):
+ timeout = config.get('who.timeout')
+ if reissue_time is None and config.get('who.reissue_time'):
+ reissue_time = config.get('who.reissue_time')
+ if timeout is not None and reissue_time is None:
+ reissue_time = int(math.ceil(int(timeout) * 0.1))
+ # Set httponly based on config value. Default is True
+ httponly = config.get('who.httponly', True)
+ # Set secure based on config value. Default is False
+ secure = config.get('who.secure', False)
+
+ # back to repoze boilerplate
+ if (secret is None and secretfile is None):
+ raise ValueError("One of 'secret' or 'secretfile' must not be None.")
+ if (secret is not None and secretfile is not None):
+ raise ValueError("Specify only one of 'secret' or 'secretfile'.")
+ if secretfile:
+ secretfile = os.path.abspath(os.path.expanduser(secretfile))
+ if not os.path.exists(secretfile):
+ raise ValueError("No such 'secretfile': %s" % secretfile)
+ secret = open(secretfile).read().strip()
+ if timeout:
+ timeout = int(timeout)
+ if reissue_time:
+ reissue_time = int(reissue_time)
+ if userid_checker is not None:
+ userid_checker = resolveDotted(userid_checker)
+ plugin = CkanAuthTktCookiePlugin(_bool(httponly),
+ secret,
+ cookie_name,
+ _bool(secure),
+ _bool(include_ip),
+ timeout,
+ reissue_time,
+ userid_checker)
+ return plugin
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/authenticator.py b/venv/lib/python2.7/site-packages/ckan/lib/authenticator.py
new file mode 100644
index 00000000..05d469fb
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/authenticator.py
@@ -0,0 +1,32 @@
+# encoding: utf-8
+
+import logging
+
+from zope.interface import implements
+from repoze.who.interfaces import IAuthenticator
+
+from ckan.model import User
+
+log = logging.getLogger(__name__)
+
+
+class UsernamePasswordAuthenticator(object):
+ implements(IAuthenticator)
+
+ def authenticate(self, environ, identity):
+ if not ('login' in identity and 'password' in identity):
+ return None
+
+ login = identity['login']
+ user = User.by_name(login)
+
+ if user is None:
+ log.debug('Login failed - username %r not found', login)
+ elif not user.is_active():
+ log.debug('Login as %r failed - user isn\'t active', login)
+ elif not user.validate_password(identity['password']):
+ log.debug('Login as %r failed - password not valid', login)
+ else:
+ return user.name
+
+ return None
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/base.py b/venv/lib/python2.7/site-packages/ckan/lib/base.py
new file mode 100644
index 00000000..62fb6076
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/base.py
@@ -0,0 +1,259 @@
+# encoding: utf-8
+
+"""The base Controller API
+
+Provides the BaseController class for subclassing.
+"""
+import logging
+import time
+import inspect
+import sys
+
+from pylons import cache
+from pylons.controllers import WSGIController
+from pylons.controllers.util import abort as _abort
+from pylons.decorators import jsonify
+from pylons.templating import cached_template, pylons_globals
+from webhelpers.html import literal
+
+from flask import (
+ render_template as flask_render_template,
+ abort as flask_abort
+)
+import ckan.exceptions
+import ckan
+import ckan.lib.i18n as i18n
+import ckan.lib.render as render_
+import ckan.lib.helpers as h
+import ckan.lib.app_globals as app_globals
+import ckan.plugins as p
+import ckan.model as model
+
+from ckan.views import (identify_user,
+ set_cors_headers_for_response,
+ check_session_cookie,
+ )
+
+# These imports are for legacy usages and will be removed soon these should
+# be imported directly from ckan.common for internal ckan code and via the
+# plugins.toolkit for extensions.
+from ckan.common import (json, _, ungettext, c, request, response, config,
+ session, is_flask_request)
+
+log = logging.getLogger(__name__)
+
+APIKEY_HEADER_NAME_KEY = 'apikey_header_name'
+APIKEY_HEADER_NAME_DEFAULT = 'X-CKAN-API-Key'
+
+
+def abort(status_code=None, detail='', headers=None, comment=None):
+ '''Abort the current request immediately by returning an HTTP exception.
+
+ This is a wrapper for :py:func:`pylons.controllers.util.abort` that adds
+ some CKAN custom behavior, including allowing
+ :py:class:`~ckan.plugins.interfaces.IAuthenticator` plugins to alter the
+ abort response, and showing flash messages in the web interface.
+
+ '''
+ if status_code == 403:
+ # Allow IAuthenticator plugins to alter the abort
+ for item in p.PluginImplementations(p.IAuthenticator):
+ result = item.abort(status_code, detail, headers, comment)
+ (status_code, detail, headers, comment) = result
+
+ if detail and status_code != 503:
+ h.flash_error(detail)
+ # #1267 Convert detail to plain text, since WebOb 0.9.7.1 (which comes
+ # with Lucid) causes an exception when unicode is received.
+ detail = detail.encode('utf8')
+ if is_flask_request():
+ flask_abort(status_code, detail)
+
+ return _abort(status_code=status_code,
+ detail=detail,
+ headers=headers,
+ comment=comment)
+
+
+def render_snippet(template_name, **kw):
+ ''' Helper function for rendering snippets. Rendered html has
+ comment tags added to show the template used. NOTE: unlike other
+ render functions this takes a list of keywords instead of a dict for
+ the extra template variables. '''
+
+ output = render(template_name, extra_vars=kw)
+ if config.get('debug'):
+ output = ('\n\n%s\n\n'
+ % (template_name, output, template_name))
+ return literal(output)
+
+
+def render_jinja2(template_name, extra_vars):
+ env = config['pylons.app_globals'].jinja_env
+ template = env.get_template(template_name)
+ return template.render(**extra_vars)
+
+
+def render(template_name, extra_vars=None, *pargs, **kwargs):
+ '''Render a template and return the output.
+
+ This is CKAN's main template rendering function.
+
+ :params template_name: relative path to template inside registered tpl_dir
+ :type template_name: str
+ :params extra_vars: additional variables available in template
+ :type extra_vars: dict
+ :params pargs: DEPRECATED
+ :type pargs: tuple
+ :params kwargs: DEPRECATED
+ :type kwargs: dict
+
+ '''
+ if pargs or kwargs:
+ tb = inspect.getframeinfo(sys._getframe(1))
+ log.warning(
+ 'Extra arguments to `base.render` are deprecated: ' +
+ '<{0.filename}:{0.lineno}>'.format(tb)
+ )
+
+ if extra_vars is None:
+ extra_vars = {}
+
+ if not is_flask_request():
+ renderer = _pylons_prepare_renderer(template_name, extra_vars,
+ *pargs, **kwargs)
+ return cached_template(template_name, renderer)
+
+ return flask_render_template(template_name, **extra_vars)
+
+
+def _pylons_prepare_renderer(template_name, extra_vars, cache_key=None,
+ cache_type=None, cache_expire=None,
+ cache_force=None, renderer=None):
+ def render_template():
+ globs = extra_vars or {}
+ globs.update(pylons_globals())
+
+ # Using pylons.url() directly destroys the localisation stuff so
+ # we remove it so any bad templates crash and burn
+ del globs['url']
+
+ try:
+ template_path, template_type = render_.template_info(template_name)
+ except render_.TemplateNotFound:
+ raise
+
+ log.debug('rendering %s [%s]' % (template_path, template_type))
+ if config.get('debug'):
+ context_vars = globs.get('c')
+ if context_vars:
+ context_vars = dir(context_vars)
+ debug_info = {'template_name': template_name,
+ 'template_path': template_path,
+ 'template_type': template_type,
+ 'vars': globs,
+ 'c_vars': context_vars,
+ 'renderer': renderer}
+ if 'CKAN_DEBUG_INFO' not in request.environ:
+ request.environ['CKAN_DEBUG_INFO'] = []
+ request.environ['CKAN_DEBUG_INFO'].append(debug_info)
+
+ del globs['config']
+ return render_jinja2(template_name, globs)
+
+ def set_pylons_response_headers(allow_cache):
+ if 'Pragma' in response.headers:
+ del response.headers["Pragma"]
+ if allow_cache:
+ response.headers["Cache-Control"] = "public"
+ try:
+ cache_expire = int(config.get('ckan.cache_expires', 0))
+ response.headers["Cache-Control"] += \
+ ", max-age=%s, must-revalidate" % cache_expire
+ except ValueError:
+ pass
+ else:
+ # We do not want caching.
+ response.headers["Cache-Control"] = "private"
+
+ # Caching Logic
+
+ allow_cache = True
+ # Force cache or not if explicit.
+ if cache_force is not None:
+ allow_cache = cache_force
+ # Do not allow caching of pages for logged in users/flash messages etc.
+ elif session.last_accessed:
+ allow_cache = False
+ # Tests etc.
+ elif 'REMOTE_USER' in request.environ:
+ allow_cache = False
+ # Don't cache if based on a non-cachable template used in this.
+ elif request.environ.get('__no_cache__'):
+ allow_cache = False
+ # Don't cache if we have set the __no_cache__ param in the query string.
+ elif request.params.get('__no_cache__'):
+ allow_cache = False
+ # Don't cache if we have extra vars containing data.
+ elif extra_vars:
+ for k, v in extra_vars.iteritems():
+ allow_cache = False
+ break
+
+ # TODO: replicate this logic in Flask once we start looking at the
+ # rendering for the frontend controllers
+ set_pylons_response_headers(allow_cache)
+
+ if not allow_cache:
+ # Prevent any further rendering from being cached.
+ request.environ['__no_cache__'] = True
+
+ return render_template
+
+
+class ValidationException(Exception):
+ pass
+
+
+class BaseController(WSGIController):
+ '''Base class for CKAN controller classes to inherit from.
+
+ '''
+ repo = model.repo
+ log = logging.getLogger(__name__)
+
+ def __before__(self, action, **params):
+ c.__timer = time.time()
+ app_globals.app_globals._check_uptodate()
+
+ identify_user()
+
+ i18n.handle_request(request, c)
+
+ def __call__(self, environ, start_response):
+ """Invoke the Controller"""
+ # WSGIController.__call__ dispatches to the Controller method
+ # the request is routed to. This routing information is
+ # available in environ['pylons.routes_dict']
+
+ try:
+ res = WSGIController.__call__(self, environ, start_response)
+ finally:
+ model.Session.remove()
+
+ check_session_cookie(response)
+
+ return res
+
+ def __after__(self, action, **params):
+
+ set_cors_headers_for_response(response)
+
+ r_time = time.time() - c.__timer
+ url = request.environ['CKAN_CURRENT_URL'].split('?')[0]
+ log.info(' %s render time %.3f seconds' % (url, r_time))
+
+
+# Include the '_' function in the public names
+__all__ = [__name for __name in locals().keys() if not __name.startswith('_')
+ or __name == '_']
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/captcha.py b/venv/lib/python2.7/site-packages/ckan/lib/captcha.py
new file mode 100644
index 00000000..9a138dff
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/captcha.py
@@ -0,0 +1,41 @@
+# encoding: utf-8
+
+from ckan.common import config
+
+import urllib
+import urllib2
+import json
+
+def check_recaptcha(request):
+ '''Check a user\'s recaptcha submission is valid, and raise CaptchaError
+ on failure.'''
+ recaptcha_private_key = config.get('ckan.recaptcha.privatekey', '')
+ if not recaptcha_private_key:
+ # Recaptcha not enabled
+ return
+
+ client_ip_address = request.environ.get('REMOTE_ADDR', 'Unknown IP Address')
+
+ # reCAPTCHA v2
+ recaptcha_response_field = request.form.get('g-recaptcha-response', '')
+ recaptcha_server_name = 'https://www.google.com/recaptcha/api/siteverify'
+
+ # recaptcha_response_field will be unicode if there are foreign chars in
+ # the user input. So we need to encode it as utf8 before urlencoding or
+ # we get an exception (#1431).
+ params = urllib.urlencode(dict(secret=recaptcha_private_key,
+ remoteip=client_ip_address,
+ response=recaptcha_response_field.encode('utf8')))
+ f = urllib2.urlopen(recaptcha_server_name, params)
+ data = json.load(f)
+ f.close()
+
+ try:
+ if not data['success']:
+ raise CaptchaError()
+ except IndexError:
+ # Something weird with recaptcha response
+ raise CaptchaError()
+
+class CaptchaError(ValueError):
+ pass
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/cli.py b/venv/lib/python2.7/site-packages/ckan/lib/cli.py
new file mode 100644
index 00000000..7135a156
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/cli.py
@@ -0,0 +1,2649 @@
+# encoding: utf-8
+
+from __future__ import print_function
+
+import collections
+import csv
+import multiprocessing as mp
+import os
+import datetime
+import sys
+from pprint import pprint
+import re
+import itertools
+import json
+import logging
+from optparse import OptionConflictError
+import traceback
+
+from six import text_type
+from six.moves import input, xrange
+from six.moves.urllib.error import HTTPError
+from six.moves.urllib.parse import urljoin, urlparse
+from six.moves.urllib.request import urlopen
+
+import sqlalchemy as sa
+import routes
+import paste.script
+from paste.registry import Registry
+from paste.script.util.logging_config import fileConfig
+import click
+
+from ckan.config.middleware import make_app
+import ckan.logic as logic
+import ckan.model as model
+import ckan.include.rjsmin as rjsmin
+import ckan.include.rcssmin as rcssmin
+import ckan.plugins as p
+from ckan.common import config
+
+# This is a test Flask request context to be used internally.
+# Do not use it!
+_cli_test_request_context = None
+
+
+# NB No CKAN imports are allowed until after the config file is loaded.
+# i.e. do the imports in methods, after _load_config is called.
+# Otherwise loggers get disabled.
+
+
+def deprecation_warning(message=None):
+ '''
+ Print a deprecation warning to STDERR.
+
+ If ``message`` is given it is also printed to STDERR.
+ '''
+ sys.stderr.write(u'WARNING: This function is deprecated.')
+ if message:
+ sys.stderr.write(u' ' + message.strip())
+ sys.stderr.write(u'\n')
+
+
+def error(msg):
+ '''
+ Print an error message to STDOUT and exit with return code 1.
+ '''
+ sys.stderr.write(msg)
+ if not msg.endswith('\n'):
+ sys.stderr.write('\n')
+ sys.exit(1)
+
+
+def parse_db_config(config_key='sqlalchemy.url'):
+ ''' Takes a config key for a database connection url and parses it into
+ a dictionary. Expects a url like:
+
+ 'postgres://tester:pass@localhost/ckantest3'
+ '''
+ from ckan.common import config
+ url = config[config_key]
+ regex = [
+ '^\s*(?P\w*)',
+ '://',
+ '(?P[^:]*)',
+ ':?',
+ '(?P[^@]*)',
+ '@',
+ '(?P[^/:]*)',
+ ':?',
+ '(?P[^/]*)',
+ '/',
+ '(?P[\w.-]*)'
+ ]
+ db_details_match = re.match(''.join(regex), url)
+ if not db_details_match:
+ raise Exception('Could not extract db details from url: %r' % url)
+ db_details = db_details_match.groupdict()
+ return db_details
+
+
+def user_add(args):
+ '''Add new user if we use paster sysadmin add
+ or paster user add
+ '''
+ if len(args) < 1:
+ error('Error: you need to specify the user name.')
+ username = args[0]
+
+ # parse args into data_dict
+ data_dict = {'name': username}
+ for arg in args[1:]:
+ try:
+ field, value = arg.split('=', 1)
+ data_dict[field] = value
+ except ValueError:
+ raise ValueError(
+ 'Could not parse arg: %r (expected "=)"' % arg
+ )
+
+ # Required
+ while '@' not in data_dict.get('email', ''):
+ data_dict['email'] = input('Email address: ').strip()
+
+ if 'password' not in data_dict:
+ data_dict['password'] = UserCmd.password_prompt()
+
+ # Optional
+ if 'fullname' in data_dict:
+ data_dict['fullname'] = data_dict['fullname'].decode(
+ sys.getfilesystemencoding()
+ )
+
+ print('Creating user: %r' % username)
+
+ try:
+ import ckan.logic as logic
+ import ckan.model as model
+ site_user = logic.get_action('get_site_user')({
+ 'model': model,
+ 'ignore_auth': True},
+ {}
+ )
+ context = {
+ 'model': model,
+ 'session': model.Session,
+ 'ignore_auth': True,
+ 'user': site_user['name'],
+ }
+ user_dict = logic.get_action('user_create')(context, data_dict)
+ pprint(user_dict)
+ except logic.ValidationError as e:
+ error(traceback.format_exc())
+
+## from http://code.activestate.com/recipes/577058/ MIT licence.
+## Written by Trent Mick
+def query_yes_no(question, default="yes"):
+ """Ask a yes/no question via input() and return their answer.
+
+ "question" is a string that is presented to the user.
+ "default" is the presumed answer if the user just hits .
+ It must be "yes" (the default), "no" or None (meaning
+ an answer is required of the user).
+
+ The "answer" return value is one of "yes" or "no".
+ """
+ valid = {"yes": "yes", "y": "yes", "ye": "yes",
+ "no": "no", "n": "no"}
+ if default is None:
+ prompt = " [y/n] "
+ elif default == "yes":
+ prompt = " [Y/n] "
+ elif default == "no":
+ prompt = " [y/N] "
+ else:
+ raise ValueError("invalid default answer: '%s'" % default)
+
+ while 1:
+ sys.stdout.write(question + prompt)
+ choice = input().strip().lower()
+ if default is not None and choice == '':
+ return default
+ elif choice in valid.keys():
+ return valid[choice]
+ else:
+ sys.stdout.write("Please respond with 'yes' or 'no' "
+ "(or 'y' or 'n').\n")
+
+
+class MockTranslator(object):
+ def gettext(self, value):
+ return value
+
+ def ugettext(self, value):
+ return value
+
+ def ungettext(self, singular, plural, n):
+ if n > 1:
+ return plural
+ return singular
+
+
+def _get_config(config=None):
+ from paste.deploy import appconfig
+
+ if config:
+ filename = os.path.abspath(config)
+ config_source = '-c parameter'
+ elif os.environ.get('CKAN_INI'):
+ filename = os.environ.get('CKAN_INI')
+ config_source = '$CKAN_INI'
+ else:
+ default_filename = 'development.ini'
+ filename = os.path.join(os.getcwd(), default_filename)
+ if not os.path.exists(filename):
+ # give really clear error message for this common situation
+ msg = 'ERROR: You need to specify the CKAN config (.ini) '\
+ 'file path.'\
+ '\nUse the --config parameter or set environment ' \
+ 'variable CKAN_INI or have {}\nin the current directory.' \
+ .format(default_filename)
+ exit(msg)
+
+ if not os.path.exists(filename):
+ msg = 'Config file not found: %s' % filename
+ msg += '\n(Given by: %s)' % config_source
+ exit(msg)
+
+ fileConfig(filename)
+ return appconfig('config:' + filename)
+
+
+def load_config(config, load_site_user=True):
+ conf = _get_config(config)
+ assert 'ckan' not in dir() # otherwise loggers would be disabled
+ # We have now loaded the config. Now we can import ckan for the
+ # first time.
+ from ckan.config.environment import load_environment
+ load_environment(conf.global_conf, conf.local_conf)
+
+ # Set this internal test request context with the configured environment so
+ # it can be used when calling url_for from the CLI.
+ global _cli_test_request_context
+
+ app = make_app(conf.global_conf, **conf.local_conf)
+ flask_app = app.apps['flask_app']._wsgi_app
+ _cli_test_request_context = flask_app.test_request_context()
+
+ registry = Registry()
+ registry.prepare()
+ import pylons
+ registry.register(pylons.translator, MockTranslator())
+
+ site_user = None
+ if model.user_table.exists() and load_site_user:
+ # If the DB has already been initialized, create and register
+ # a pylons context object, and add the site user to it, so the
+ # auth works as in a normal web request
+ c = pylons.util.AttribSafeContextObj()
+
+ registry.register(pylons.c, c)
+
+ site_user = logic.get_action('get_site_user')({'ignore_auth': True}, {})
+
+ pylons.c.user = site_user['name']
+ pylons.c.userobj = model.User.get(site_user['name'])
+
+ ## give routes enough information to run url_for
+ parsed = urlparse(conf.get('ckan.site_url', 'http://0.0.0.0'))
+ request_config = routes.request_config()
+ request_config.host = parsed.netloc + parsed.path
+ request_config.protocol = parsed.scheme
+
+ return site_user
+
+
+def paster_click_group(summary):
+ '''Return a paster command click.Group for paster subcommands
+
+ :param command: the paster command linked to this function from
+ setup.py, used in help text (e.g. "datastore")
+ :param summary: summary text used in paster's help/command listings
+ (e.g. "Perform commands to set up the datastore")
+ '''
+ class PasterClickGroup(click.Group):
+ '''A click.Group that may be called like a paster command'''
+ def __call__(self, ignored_command):
+ sys.argv.remove(ignored_command)
+ return super(PasterClickGroup, self).__call__(
+ prog_name=u'paster ' + ignored_command,
+ help_option_names=[u'-h', u'--help'],
+ obj={})
+
+ @click.group(cls=PasterClickGroup)
+ @click.option(
+ '--plugin',
+ metavar='ckan',
+ help='paster plugin (when run outside ckan directory)')
+ @click_config_option
+ @click.pass_context
+ def cli(ctx, plugin, config):
+ ctx.obj['config'] = config
+
+
+ cli.summary = summary
+ cli.group_name = u'ckan'
+ return cli
+
+
+# common definition for paster ... --config
+click_config_option = click.option(
+ '-c',
+ '--config',
+ default=None,
+ metavar='CONFIG',
+ help=u'Config file to use (default: development.ini)')
+
+
+class CkanCommand(paste.script.command.Command):
+ '''Base class for classes that implement CKAN paster commands to inherit.'''
+ parser = paste.script.command.Command.standard_parser(verbose=True)
+ parser.add_option('-c', '--config', dest='config',
+ help='Config file to use.')
+ parser.add_option('-f', '--file',
+ action='store',
+ dest='file_path',
+ help="File to dump results to (if needed)")
+ default_verbosity = 1
+ group_name = 'ckan'
+
+ def _load_config(self, load_site_user=True):
+ self.site_user = load_config(self.options.config, load_site_user)
+
+
+class ManageDb(CkanCommand):
+ '''Perform various tasks on the database.
+
+ db create - alias of db upgrade
+ db init - create and put in default data
+ db clean - clears db (including dropping tables) and
+ search index
+ db upgrade [version no.] - Data migrate
+ db version - returns current version of data schema
+ db dump FILE_PATH - dump to a pg_dump file [DEPRECATED]
+ db load FILE_PATH - load a pg_dump from a file [DEPRECATED]
+ db load-only FILE_PATH - load a pg_dump from a file but don\'t do
+ the schema upgrade or search indexing [DEPRECATED]
+ db create-from-model - create database from the model (indexes not made)
+ db migrate-filestore - migrate all uploaded data from the 2.1 filesore.
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = None
+ min_args = 1
+
+ def command(self):
+ cmd = self.args[0]
+
+ self._load_config(cmd!='upgrade')
+ import ckan.model as model
+ import ckan.lib.search as search
+
+ if cmd == 'init':
+
+ model.repo.init_db()
+ if self.verbose:
+ print('Initialising DB: SUCCESS')
+ elif cmd == 'clean' or cmd == 'drop':
+
+ # remove any *.pyc version files to prevent conflicts
+ v_path = os.path.join(os.path.dirname(__file__),
+ '..', 'migration', 'versions', '*.pyc')
+ import glob
+ filelist = glob.glob(v_path)
+ for f in filelist:
+ os.remove(f)
+
+ model.repo.clean_db()
+ search.clear_all()
+ if self.verbose:
+ print('Cleaning DB: SUCCESS')
+ elif cmd == 'upgrade':
+ if len(self.args) > 1:
+ model.repo.upgrade_db(self.args[1])
+ else:
+ model.repo.upgrade_db()
+ elif cmd == 'version':
+ self.version()
+ elif cmd == 'dump':
+ self.dump()
+ elif cmd == 'load':
+ self.load()
+ elif cmd == 'load-only':
+ self.load(only_load=True)
+ elif cmd == 'create-from-model':
+ model.repo.create_db()
+ if self.verbose:
+ print('Creating DB: SUCCESS')
+ elif cmd == 'migrate-filestore':
+ self.migrate_filestore()
+ else:
+ error('Command %s not recognized' % cmd)
+
+ def _get_db_config(self):
+ return parse_db_config()
+
+ def _get_postgres_cmd(self, command):
+ self.db_details = self._get_db_config()
+ if self.db_details.get('db_type') not in ('postgres', 'postgresql'):
+ raise AssertionError('Expected postgres database - not %r' % self.db_details.get('db_type'))
+ pg_cmd = command
+ pg_cmd += ' -U %(db_user)s' % self.db_details
+ if self.db_details.get('db_pass') not in (None, ''):
+ pg_cmd = 'export PGPASSWORD=%(db_pass)s && ' % self.db_details + pg_cmd
+ if self.db_details.get('db_host') not in (None, ''):
+ pg_cmd += ' -h %(db_host)s' % self.db_details
+ if self.db_details.get('db_port') not in (None, ''):
+ pg_cmd += ' -p %(db_port)s' % self.db_details
+ return pg_cmd
+
+ def _get_psql_cmd(self):
+ psql_cmd = self._get_postgres_cmd('psql')
+ psql_cmd += ' -d %(db_name)s' % self.db_details
+ return psql_cmd
+
+ def _postgres_dump(self, filepath):
+ pg_dump_cmd = self._get_postgres_cmd('pg_dump')
+ pg_dump_cmd += ' %(db_name)s' % self.db_details
+ pg_dump_cmd += ' > %s' % filepath
+ self._run_cmd(pg_dump_cmd)
+ print('Dumped database to: %s' % filepath)
+
+ def _postgres_load(self, filepath):
+ import ckan.model as model
+ assert not model.repo.are_tables_created(), "Tables already found. You need to 'db clean' before a load."
+ pg_cmd = self._get_psql_cmd() + ' -f %s' % filepath
+ self._run_cmd(pg_cmd)
+ print('Loaded CKAN database: %s' % filepath)
+
+ def _run_cmd(self, command_line):
+ import subprocess
+ retcode = subprocess.call(command_line, shell=True)
+ if retcode != 0:
+ raise SystemError('Command exited with errorcode: %i' % retcode)
+
+ def dump(self):
+ deprecation_warning(u"Use PostgreSQL's pg_dump instead.")
+ if len(self.args) < 2:
+ print('Need pg_dump filepath')
+ return
+ dump_path = self.args[1]
+
+ psql_cmd = self._get_psql_cmd() + ' -f %s'
+ pg_cmd = self._postgres_dump(dump_path)
+
+ def load(self, only_load=False):
+ deprecation_warning(u"Use PostgreSQL's pg_restore instead.")
+ if len(self.args) < 2:
+ print('Need pg_dump filepath')
+ return
+ dump_path = self.args[1]
+
+ psql_cmd = self._get_psql_cmd() + ' -f %s'
+ pg_cmd = self._postgres_load(dump_path)
+ if not only_load:
+ print('Upgrading DB')
+ import ckan.model as model
+ model.repo.upgrade_db()
+
+ print('Rebuilding search index')
+ import ckan.lib.search
+ ckan.lib.search.rebuild()
+ else:
+ print('Now remember you have to call \'db upgrade\' and then \'search-index rebuild\'.')
+ print('Done')
+
+ def migrate_filestore(self):
+ from ckan.model import Session
+ import requests
+ from ckan.lib.uploader import ResourceUpload
+ results = Session.execute("select id, revision_id, url from resource "
+ "where resource_type = 'file.upload' "
+ "and (url_type <> 'upload' or url_type is null)"
+ "and url like '%storage%'")
+ for id, revision_id, url in results:
+ response = requests.get(url, stream=True)
+ if response.status_code != 200:
+ print("failed to fetch %s (code %s)" % (url,
+ response.status_code))
+ continue
+ resource_upload = ResourceUpload({'id': id})
+ assert resource_upload.storage_path, "no storage configured aborting"
+
+ directory = resource_upload.get_directory(id)
+ filepath = resource_upload.get_path(id)
+ try:
+ os.makedirs(directory)
+ except OSError as e:
+ ## errno 17 is file already exists
+ if e.errno != 17:
+ raise
+
+ with open(filepath, 'wb+') as out:
+ for chunk in response.iter_content(1024):
+ if chunk:
+ out.write(chunk)
+
+ Session.execute("update resource set url_type = 'upload'"
+ "where id = :id", {'id': id})
+ Session.execute("update resource_revision set url_type = 'upload'"
+ "where id = :id and "
+ "revision_id = :revision_id",
+ {'id': id, 'revision_id': revision_id})
+ Session.commit()
+ print("Saved url %s" % url)
+
+ def version(self):
+ from ckan.model import Session
+ print(Session.execute('select version from '
+ 'migrate_version;').fetchall())
+
+
+class SearchIndexCommand(CkanCommand):
+ '''Creates a search index for all datasets
+
+ Usage:
+ search-index [-i] [-o] [-r] [-e] [-q] rebuild [dataset_name] - reindex dataset_name if given, if not then rebuild
+ full search index (all datasets)
+ search-index rebuild_fast - reindex using multiprocessing using all cores.
+ This acts in the same way as rubuild -r [EXPERIMENTAL]
+ search-index check - checks for datasets not indexed
+ search-index show DATASET_NAME - shows index of a dataset
+ search-index clear [dataset_name] - clears the search index for the provided dataset or
+ for the whole ckan instance
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 2
+ min_args = 0
+
+ def __init__(self, name):
+ super(SearchIndexCommand, self).__init__(name)
+
+ self.parser.add_option('-i', '--force', dest='force',
+ action='store_true', default=False,
+ help='Ignore exceptions when rebuilding the index')
+
+ self.parser.add_option('-o', '--only-missing', dest='only_missing',
+ action='store_true', default=False,
+ help='Index non indexed datasets only')
+
+ self.parser.add_option('-r', '--refresh', dest='refresh',
+ action='store_true', default=False,
+ help='Refresh current index (does not clear the existing one)')
+
+ self.parser.add_option('-q', '--quiet', dest='quiet',
+ action='store_true', default=False,
+ help='Do not output index rebuild progress')
+
+ self.parser.add_option('-e', '--commit-each', dest='commit_each',
+ action='store_true', default=False, help=
+'''Perform a commit after indexing each dataset. This ensures that changes are
+immediately available on the search, but slows significantly the process.
+Default is false.''')
+
+ def command(self):
+ if not self.args:
+ # default to printing help
+ print(self.usage)
+ return
+
+ cmd = self.args[0]
+ # Do not run load_config yet
+ if cmd == 'rebuild_fast':
+ self.rebuild_fast()
+ return
+
+ self._load_config()
+ if cmd == 'rebuild':
+ self.rebuild()
+ elif cmd == 'check':
+ self.check()
+ elif cmd == 'show':
+ self.show()
+ elif cmd == 'clear':
+ self.clear()
+ else:
+ print('Command %s not recognized' % cmd)
+
+ def rebuild(self):
+ from ckan.lib.search import rebuild, commit
+
+ # BY default we don't commit after each request to Solr, as it is
+ # a really heavy operation and slows things a lot
+
+ if len(self.args) > 1:
+ rebuild(self.args[1])
+ else:
+ rebuild(only_missing=self.options.only_missing,
+ force=self.options.force,
+ refresh=self.options.refresh,
+ defer_commit=(not self.options.commit_each),
+ quiet=self.options.quiet)
+
+ if not self.options.commit_each:
+ commit()
+
+ def check(self):
+ from ckan.lib.search import check
+ check()
+
+ def show(self):
+ from ckan.lib.search import show
+
+ if not len(self.args) == 2:
+ print('Missing parameter: dataset-name')
+ return
+ index = show(self.args[1])
+ pprint(index)
+
+ def clear(self):
+ from ckan.lib.search import clear, clear_all
+ package_id = self.args[1] if len(self.args) > 1 else None
+ if not package_id:
+ clear_all()
+ else:
+ clear(package_id)
+
+ def rebuild_fast(self):
+ ### Get out config but without starting pylons environment ####
+ conf = _get_config(self.options.config)
+
+ ### Get ids using own engine, otherwise multiprocess will balk
+ db_url = conf['sqlalchemy.url']
+ engine = sa.create_engine(db_url)
+ package_ids = []
+ result = engine.execute("select id from package where state = 'active';")
+ for row in result:
+ package_ids.append(row[0])
+
+ def start(ids):
+ ## load actual enviroment for each subprocess, so each have thier own
+ ## sa session
+ self._load_config()
+ from ckan.lib.search import rebuild, commit
+ rebuild(package_ids=ids)
+ commit()
+
+ def chunks(l, n):
+ """ Yield n successive chunks from l.
+ """
+ newn = int(len(l) / n)
+ for i in xrange(0, n-1):
+ yield l[i*newn:i*newn+newn]
+ yield l[n*newn-newn:]
+
+ processes = []
+ for chunk in chunks(package_ids, mp.cpu_count()):
+ process = mp.Process(target=start, args=(chunk,))
+ processes.append(process)
+ process.daemon = True
+ process.start()
+
+ for process in processes:
+ process.join()
+
+
+class Notification(CkanCommand):
+ '''Send out modification notifications.
+
+ In "replay" mode, an update signal is sent for each dataset in the database.
+
+ Usage:
+ notify replay - send out modification signals
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 1
+ min_args = 0
+
+ def command(self):
+ self._load_config()
+ from ckan.model import Session, Package, DomainObjectOperation
+ from ckan.model.modification import DomainObjectModificationExtension
+
+ if not self.args:
+ # default to run
+ cmd = 'replay'
+ else:
+ cmd = self.args[0]
+
+ if cmd == 'replay':
+ dome = DomainObjectModificationExtension()
+ for package in Session.query(Package):
+ dome.notify(package, DomainObjectOperation.changed)
+ else:
+ print('Command %s not recognized' % cmd)
+
+
+class RDFExport(CkanCommand):
+ '''Export active datasets as RDF
+ This command dumps out all currently active datasets as RDF into the
+ specified folder.
+
+ Usage:
+ paster rdf-export /path/to/store/output
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+
+ def command(self):
+ self._load_config()
+
+ if not self.args:
+ # default to run
+ print(RDFExport.__doc__)
+ else:
+ self.export_datasets(self.args[0])
+
+ def export_datasets(self, out_folder):
+ '''
+ Export datasets as RDF to an output folder.
+ '''
+ from ckan.common import config
+ import ckan.model as model
+ import ckan.logic as logic
+ import ckan.lib.helpers as h
+
+ # Create output folder if not exists
+ if not os.path.isdir(out_folder):
+ os.makedirs(out_folder)
+
+ fetch_url = config['ckan.site_url']
+ user = logic.get_action('get_site_user')({'model': model, 'ignore_auth': True}, {})
+ context = {'model': model, 'session': model.Session, 'user': user['name']}
+ dataset_names = logic.get_action('package_list')(context, {})
+ for dataset_name in dataset_names:
+ dd = logic.get_action('package_show')(context, {'id': dataset_name})
+ if not dd['state'] == 'active':
+ continue
+
+ url = h.url_for(controller='package', action='read', id=dd['name'])
+
+ url = urljoin(fetch_url, url[1:]) + '.rdf'
+ try:
+ fname = os.path.join(out_folder, dd['name']) + ".rdf"
+ try:
+ r = urlopen(url).read()
+ except HTTPError as e:
+ if e.code == 404:
+ error('Please install ckanext-dcat and enable the ' +
+ '`dcat` plugin to use the RDF serializations')
+ with open(fname, 'wb') as f:
+ f.write(r)
+ except IOError as ioe:
+ sys.stderr.write(str(ioe) + "\n")
+
+
+class Sysadmin(CkanCommand):
+ '''Gives sysadmin rights to a named user
+
+ Usage:
+ sysadmin - lists sysadmins
+ sysadmin list - lists sysadmins
+ sysadmin add USERNAME - make an existing user into a sysadmin
+ sysadmin add USERNAME [FIELD1=VALUE1 FIELD2=VALUE2 ...]
+ - creates a new user that is a sysadmin
+ (prompts for password and email if not
+ supplied).
+ Field can be: apikey
+ password
+ email
+ sysadmin remove USERNAME - removes user from sysadmins
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = None
+ min_args = 0
+
+ def command(self):
+ self._load_config()
+
+ cmd = self.args[0] if self.args else None
+ if cmd is None or cmd == 'list':
+ self.list()
+ elif cmd == 'add':
+ self.add()
+ elif cmd == 'remove':
+ self.remove()
+ else:
+ print('Command %s not recognized' % cmd)
+
+ def list(self):
+ import ckan.model as model
+ print('Sysadmins:')
+ sysadmins = model.Session.query(model.User).filter_by(sysadmin=True,
+ state='active')
+ print('count = %i' % sysadmins.count())
+ for sysadmin in sysadmins:
+ print('%s name=%s email=%s id=%s' % (
+ sysadmin.__class__.__name__,
+ sysadmin.name,
+ sysadmin.email,
+ sysadmin.id))
+
+ def add(self):
+ import ckan.model as model
+
+ if len(self.args) < 2:
+ print('Need name of the user to be made sysadmin.')
+ return
+ username = self.args[1]
+
+ user = model.User.by_name(text_type(username))
+ if not user:
+ print('User "%s" not found' % username)
+ makeuser = input('Create new user: %s? [y/n]' % username)
+ if makeuser == 'y':
+ user_add(self.args[1:])
+ user = model.User.by_name(text_type(username))
+ else:
+ print('Exiting ...')
+ return
+
+ user.sysadmin = True
+ model.Session.add(user)
+ model.repo.commit_and_remove()
+ print('Added %s as sysadmin' % username)
+
+ def remove(self):
+ import ckan.model as model
+
+ if len(self.args) < 2:
+ print('Need name of the user to be made sysadmin.')
+ return
+ username = self.args[1]
+
+ user = model.User.by_name(text_type(username))
+ if not user:
+ print('Error: user "%s" not found!' % username)
+ return
+ user.sysadmin = False
+ model.repo.commit_and_remove()
+
+
+class UserCmd(CkanCommand):
+ '''Manage users
+
+ Usage:
+ user - lists users
+ user list - lists users
+ user USERNAME - shows user properties
+ user add USERNAME [FIELD1=VALUE1 FIELD2=VALUE2 ...]
+ - add a user (prompts for email and
+ password if not supplied).
+ Field can be: apikey
+ password
+ email
+ user setpass USERNAME - set user password (prompts)
+ user remove USERNAME - removes user from users
+ user search QUERY - searches for a user name
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = None
+ min_args = 0
+
+ def command(self):
+ self._load_config()
+
+ if not self.args:
+ self.list()
+ else:
+ cmd = self.args[0]
+ if cmd == 'add':
+ self.add()
+ elif cmd == 'remove':
+ self.remove()
+ elif cmd == 'search':
+ self.search()
+ elif cmd == 'setpass':
+ self.setpass()
+ elif cmd == 'list':
+ self.list()
+ else:
+ self.show()
+
+ def get_user_str(self, user):
+ user_str = 'name=%s' % user.name
+ if user.name != user.display_name:
+ user_str += ' display=%s' % user.display_name
+ return user_str
+
+ def list(self):
+ import ckan.model as model
+ print('Users:')
+ users = model.Session.query(model.User).filter_by(state='active')
+ print('count = %i' % users.count())
+ for user in users:
+ print(self.get_user_str(user))
+
+ def show(self):
+ import ckan.model as model
+
+ username = self.args[0]
+ user = model.User.get(text_type(username))
+ print('User: \n', user)
+
+ def setpass(self):
+ import ckan.model as model
+
+ if len(self.args) < 2:
+ print('Need name of the user.')
+ return
+ username = self.args[1]
+ user = model.User.get(username)
+ print('Editing user: %r' % user.name)
+
+ password = self.password_prompt()
+ user.password = password
+ model.repo.commit_and_remove()
+ print('Done')
+
+ def search(self):
+ import ckan.model as model
+
+ if len(self.args) < 2:
+ print('Need user name query string.')
+ return
+ query_str = self.args[1]
+
+ query = model.User.search(query_str)
+ print('%i users matching %r:' % (query.count(), query_str))
+ for user in query.all():
+ print(self.get_user_str(user))
+
+ @classmethod
+ def password_prompt(cls):
+ import getpass
+ password1 = None
+ while not password1:
+ password1 = getpass.getpass('Password: ')
+ password2 = getpass.getpass('Confirm password: ')
+ if password1 != password2:
+ error('Passwords do not match')
+ return password1
+
+ def add(self):
+ user_add(self.args[1:])
+
+ def remove(self):
+ import ckan.model as model
+
+ if len(self.args) < 2:
+ print('Need name of the user.')
+ return
+ username = self.args[1]
+
+ p.toolkit.get_action('user_delete')(
+ {'model': model, 'ignore_auth': True},
+ {'id': username})
+ print('Deleted user: %s' % username)
+
+
+class DatasetCmd(CkanCommand):
+ '''Manage datasets
+
+ Usage:
+ dataset DATASET_NAME|ID - shows dataset properties
+ dataset show DATASET_NAME|ID - shows dataset properties
+ dataset list - lists datasets
+ dataset delete [DATASET_NAME|ID] - changes dataset state to 'deleted'
+ dataset purge [DATASET_NAME|ID] - removes dataset from db entirely
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 3
+ min_args = 0
+
+ def command(self):
+ self._load_config()
+
+ if not self.args:
+ print(self.usage)
+ else:
+ cmd = self.args[0]
+ if cmd == 'delete':
+ self.delete(self.args[1])
+ elif cmd == 'purge':
+ self.purge(self.args[1])
+ elif cmd == 'list':
+ self.list()
+ elif cmd == 'show':
+ self.show(self.args[1])
+ else:
+ self.show(self.args[0])
+
+ def list(self):
+ import ckan.model as model
+ print('Datasets:')
+ datasets = model.Session.query(model.Package)
+ print('count = %i' % datasets.count())
+ for dataset in datasets:
+ state = ('(%s)' % dataset.state) if dataset.state != 'active' else ''
+ print('%s %s %s' % (dataset.id, dataset.name, state))
+
+ def _get_dataset(self, dataset_ref):
+ import ckan.model as model
+ dataset = model.Package.get(text_type(dataset_ref))
+ assert dataset, 'Could not find dataset matching reference: %r' % dataset_ref
+ return dataset
+
+ def show(self, dataset_ref):
+ import pprint
+ dataset = self._get_dataset(dataset_ref)
+ pprint.pprint(dataset.as_dict())
+
+ def delete(self, dataset_ref):
+ import ckan.model as model
+ dataset = self._get_dataset(dataset_ref)
+ old_state = dataset.state
+
+ rev = model.repo.new_revision()
+ dataset.delete()
+ model.repo.commit_and_remove()
+ dataset = self._get_dataset(dataset_ref)
+ print('%s %s -> %s' % (dataset.name, old_state, dataset.state))
+
+ def purge(self, dataset_ref):
+ import ckan.logic as logic
+ dataset = self._get_dataset(dataset_ref)
+ name = dataset.name
+
+ site_user = logic.get_action('get_site_user')({'ignore_auth': True}, {})
+ context = {'user': site_user['name']}
+ logic.get_action('dataset_purge')(
+ context, {'id': dataset_ref})
+ print('%s purged' % name)
+
+
+class Ratings(CkanCommand):
+ '''Manage the ratings stored in the db
+
+ Usage:
+ ratings count - counts ratings
+ ratings clean - remove all ratings
+ ratings clean-anonymous - remove only anonymous ratings
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 1
+ min_args = 1
+
+ def command(self):
+ self._load_config()
+ import ckan.model as model
+
+ cmd = self.args[0]
+ if cmd == 'count':
+ self.count()
+ elif cmd == 'clean':
+ self.clean()
+ elif cmd == 'clean-anonymous':
+ self.clean(user_ratings=False)
+ else:
+ print('Command %s not recognized' % cmd)
+
+ def count(self):
+ import ckan.model as model
+ q = model.Session.query(model.Rating)
+ print("%i ratings" % q.count())
+ q = q.filter(model.Rating.user_id is None)
+ print("of which %i are anonymous ratings" % q.count())
+
+ def clean(self, user_ratings=True):
+ import ckan.model as model
+ q = model.Session.query(model.Rating)
+ print("%i ratings" % q.count())
+ if not user_ratings:
+ q = q.filter(model.Rating.user_id is None)
+ print("of which %i are anonymous ratings" % q.count())
+ ratings = q.all()
+ for rating in ratings:
+ rating.purge()
+ model.repo.commit_and_remove()
+
+
+## Used by the Tracking class
+_ViewCount = collections.namedtuple("ViewCount", "id name count")
+
+
+class Tracking(CkanCommand):
+ '''Update tracking statistics
+
+ Usage:
+ tracking update [start_date] - update tracking stats
+ tracking export FILE [start_date] - export tracking stats to a csv file
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 3
+ min_args = 1
+
+ def command(self):
+ self._load_config()
+ import ckan.model as model
+ engine = model.meta.engine
+
+ cmd = self.args[0]
+ if cmd == 'update':
+ start_date = self.args[1] if len(self.args) > 1 else None
+ self.update_all(engine, start_date)
+ elif cmd == 'export':
+ if len(self.args) <= 1:
+ error(self.__class__.__doc__)
+ output_file = self.args[1]
+ start_date = self.args[2] if len(self.args) > 2 else None
+ self.update_all(engine, start_date)
+ self.export_tracking(engine, output_file)
+ else:
+ error(self.__class__.__doc__)
+
+ def update_all(self, engine, start_date=None):
+ if start_date:
+ start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d')
+ else:
+ # No date given. See when we last have data for and get data
+ # from 2 days before then in case new data is available.
+ # If no date here then use 2011-01-01 as the start date
+ sql = '''SELECT tracking_date from tracking_summary
+ ORDER BY tracking_date DESC LIMIT 1;'''
+ result = engine.execute(sql).fetchall()
+ if result:
+ start_date = result[0]['tracking_date']
+ start_date += datetime.timedelta(-2)
+ # convert date to datetime
+ combine = datetime.datetime.combine
+ start_date = combine(start_date, datetime.time(0))
+ else:
+ start_date = datetime.datetime(2011, 1, 1)
+ start_date_solrsync = start_date
+ end_date = datetime.datetime.now()
+
+ while start_date < end_date:
+ stop_date = start_date + datetime.timedelta(1)
+ self.update_tracking(engine, start_date)
+ print('tracking updated for %s' % start_date)
+ start_date = stop_date
+
+ self.update_tracking_solr(engine, start_date_solrsync)
+
+ def _total_views(self, engine):
+ sql = '''
+ SELECT p.id,
+ p.name,
+ COALESCE(SUM(s.count), 0) AS total_views
+ FROM package AS p
+ LEFT OUTER JOIN tracking_summary AS s ON s.package_id = p.id
+ GROUP BY p.id, p.name
+ ORDER BY total_views DESC
+ '''
+ return [_ViewCount(*t) for t in engine.execute(sql).fetchall()]
+
+ def _recent_views(self, engine, measure_from):
+ sql = '''
+ SELECT p.id,
+ p.name,
+ COALESCE(SUM(s.count), 0) AS total_views
+ FROM package AS p
+ LEFT OUTER JOIN tracking_summary AS s ON s.package_id = p.id
+ WHERE s.tracking_date >= %(measure_from)s
+ GROUP BY p.id, p.name
+ ORDER BY total_views DESC
+ '''
+ return [_ViewCount(*t) for t in engine.execute(sql, measure_from=str(measure_from)).fetchall()]
+
+ def export_tracking(self, engine, output_filename):
+ '''Write tracking summary to a csv file.'''
+ HEADINGS = [
+ "dataset id",
+ "dataset name",
+ "total views",
+ "recent views (last 2 weeks)",
+ ]
+
+ measure_from = datetime.date.today() - datetime.timedelta(days=14)
+ recent_views = self._recent_views(engine, measure_from)
+ total_views = self._total_views(engine)
+
+ with open(output_filename, 'w') as fh:
+ f_out = csv.writer(fh)
+ f_out.writerow(HEADINGS)
+ recent_views_for_id = dict((r.id, r.count) for r in recent_views)
+ f_out.writerows([(r.id,
+ r.name,
+ r.count,
+ recent_views_for_id.get(r.id, 0))
+ for r in total_views])
+
+ def update_tracking(self, engine, summary_date):
+ PACKAGE_URL = '/dataset/'
+ # clear out existing data before adding new
+ sql = '''DELETE FROM tracking_summary
+ WHERE tracking_date='%s'; ''' % summary_date
+ engine.execute(sql)
+
+ sql = '''SELECT DISTINCT url, user_key,
+ CAST(access_timestamp AS Date) AS tracking_date,
+ tracking_type INTO tracking_tmp
+ FROM tracking_raw
+ WHERE CAST(access_timestamp as Date)=%s;
+
+ INSERT INTO tracking_summary
+ (url, count, tracking_date, tracking_type)
+ SELECT url, count(user_key), tracking_date, tracking_type
+ FROM tracking_tmp
+ GROUP BY url, tracking_date, tracking_type;
+
+ DROP TABLE tracking_tmp;
+ COMMIT;'''
+ engine.execute(sql, summary_date)
+
+ # get ids for dataset urls
+ sql = '''UPDATE tracking_summary t
+ SET package_id = COALESCE(
+ (SELECT id FROM package p
+ WHERE p.name = regexp_replace(' ' || t.url, '^[ ]{1}(/\w{2}){0,1}' || %s, ''))
+ ,'~~not~found~~')
+ WHERE t.package_id IS NULL
+ AND tracking_type = 'page';'''
+ engine.execute(sql, PACKAGE_URL)
+
+ # update summary totals for resources
+ sql = '''UPDATE tracking_summary t1
+ SET running_total = (
+ SELECT sum(count)
+ FROM tracking_summary t2
+ WHERE t1.url = t2.url
+ AND t2.tracking_date <= t1.tracking_date
+ )
+ ,recent_views = (
+ SELECT sum(count)
+ FROM tracking_summary t2
+ WHERE t1.url = t2.url
+ AND t2.tracking_date <= t1.tracking_date AND t2.tracking_date >= t1.tracking_date - 14
+ )
+ WHERE t1.running_total = 0 AND tracking_type = 'resource';'''
+ engine.execute(sql)
+
+ # update summary totals for pages
+ sql = '''UPDATE tracking_summary t1
+ SET running_total = (
+ SELECT sum(count)
+ FROM tracking_summary t2
+ WHERE t1.package_id = t2.package_id
+ AND t2.tracking_date <= t1.tracking_date
+ )
+ ,recent_views = (
+ SELECT sum(count)
+ FROM tracking_summary t2
+ WHERE t1.package_id = t2.package_id
+ AND t2.tracking_date <= t1.tracking_date AND t2.tracking_date >= t1.tracking_date - 14
+ )
+ WHERE t1.running_total = 0 AND tracking_type = 'page'
+ AND t1.package_id IS NOT NULL
+ AND t1.package_id != '~~not~found~~';'''
+ engine.execute(sql)
+
+ def update_tracking_solr(self, engine, start_date):
+ sql = '''SELECT package_id FROM tracking_summary
+ where package_id!='~~not~found~~'
+ and tracking_date >= %s;'''
+ results = engine.execute(sql, start_date)
+
+ package_ids = set()
+ for row in results:
+ package_ids.add(row['package_id'])
+
+ total = len(package_ids)
+ not_found = 0
+ print('%i package index%s to be rebuilt starting from %s' % (total, '' if total < 2 else 'es', start_date))
+
+ from ckan.lib.search import rebuild
+ for package_id in package_ids:
+ try:
+ rebuild(package_id)
+ except logic.NotFound:
+ print("Error: package %s not found." % (package_id))
+ not_found += 1
+ except KeyboardInterrupt:
+ print("Stopped.")
+ return
+ except:
+ raise
+ print('search index rebuilding done.' + (' %i not found.' % (not_found) if not_found else ""))
+
+
+class PluginInfo(CkanCommand):
+ '''Provide info on installed plugins.
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 0
+ min_args = 0
+
+ def command(self):
+ self.get_info()
+
+ def get_info(self):
+ ''' print info about current plugins from the .ini file'''
+ import ckan.plugins as p
+ self._load_config()
+ interfaces = {}
+ plugins = {}
+ for name in dir(p):
+ item = getattr(p, name)
+ try:
+ if issubclass(item, p.Interface):
+ interfaces[item] = {'class': item}
+ except TypeError:
+ pass
+
+ for interface in interfaces:
+ for plugin in p.PluginImplementations(interface):
+ name = plugin.name
+ if name not in plugins:
+ plugins[name] = {'doc': plugin.__doc__,
+ 'class': plugin,
+ 'implements': []}
+ plugins[name]['implements'].append(interface.__name__)
+
+ for plugin in plugins:
+ p = plugins[plugin]
+ print(plugin + ':')
+ print('-' * (len(plugin) + 1))
+ if p['doc']:
+ print(p['doc'])
+ print('Implements:')
+ for i in p['implements']:
+ extra = None
+ if i == 'ITemplateHelpers':
+ extra = self.template_helpers(p['class'])
+ if i == 'IActions':
+ extra = self.actions(p['class'])
+ print(' %s' % i)
+ if extra:
+ print(extra)
+ print
+
+ def actions(self, cls):
+ ''' Return readable action function info. '''
+ actions = cls.get_actions()
+ return self.function_info(actions)
+
+ def template_helpers(self, cls):
+ ''' Return readable helper function info. '''
+ helpers = cls.get_helpers()
+ return self.function_info(helpers)
+
+ def function_info(self, functions):
+ ''' Take a dict of functions and output readable info '''
+ import inspect
+ output = []
+ for function_name in functions:
+ fn = functions[function_name]
+ args_info = inspect.getargspec(fn)
+ params = args_info.args
+ num_params = len(params)
+ if args_info.varargs:
+ params.append('*' + args_info.varargs)
+ if args_info.keywords:
+ params.append('**' + args_info.keywords)
+ if args_info.defaults:
+ offset = num_params - len(args_info.defaults)
+ for i, v in enumerate(args_info.defaults):
+ params[i + offset] = params[i + offset] + '=' + repr(v)
+ # is this a classmethod if so remove the first parameter
+ if inspect.ismethod(fn) and inspect.isclass(fn.__self__):
+ params = params[1:]
+ params = ', '.join(params)
+ output.append(' %s(%s)' % (function_name, params))
+ # doc string
+ if fn.__doc__:
+ bits = fn.__doc__.split('\n')
+ for bit in bits:
+ output.append(' %s' % bit)
+ return ('\n').join(output)
+
+
+class CreateTestDataCommand(CkanCommand):
+ '''Create test data in the database.
+ Tests can also delete the created objects easily with the delete() method.
+
+ create-test-data - annakarenina and warandpeace
+ create-test-data search - realistic data to test search
+ create-test-data gov - government style data
+ create-test-data family - package relationships data
+ create-test-data user - create a user 'tester' with api key 'tester'
+ create-test-data translations - annakarenina, warandpeace, and some test
+ translations of terms
+ create-test-data vocabs - annakerenina, warandpeace, and some test
+ vocabularies
+ create-test-data hierarchy - hierarchy of groups
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 1
+ min_args = 0
+
+ def command(self):
+ self._load_config()
+ from ckan import plugins
+ from create_test_data import CreateTestData
+
+ if self.args:
+ cmd = self.args[0]
+ else:
+ cmd = 'basic'
+ if self.verbose:
+ print('Creating %s test data' % cmd)
+ if cmd == 'basic':
+ CreateTestData.create_basic_test_data()
+ elif cmd == 'user':
+ CreateTestData.create_test_user()
+ print('Created user %r with password %r and apikey %r' %
+ ('tester', 'tester', 'tester'))
+ elif cmd == 'search':
+ CreateTestData.create_search_test_data()
+ elif cmd == 'gov':
+ CreateTestData.create_gov_test_data()
+ elif cmd == 'family':
+ CreateTestData.create_family_test_data()
+ elif cmd == 'translations':
+ CreateTestData.create_translations_test_data()
+ elif cmd == 'vocabs':
+ CreateTestData.create_vocabs_test_data()
+ elif cmd == 'hierarchy':
+ CreateTestData.create_group_hierarchy_test_data()
+ else:
+ print('Command %s not recognized' % cmd)
+ raise NotImplementedError
+ if self.verbose:
+ print('Creating %s test data: Complete!' % cmd)
+
+
+class Profile(CkanCommand):
+ '''Code speed profiler
+ Provide a ckan url and it will make the request and record
+ how long each function call took in a file that can be read
+ by pstats.Stats (command-line) or runsnakerun (gui).
+
+ Usage:
+ profile URL [username]
+
+ e.g. profile /data/search
+
+ The result is saved in profile.data.search
+ To view the profile in runsnakerun:
+ runsnakerun ckan.data.search.profile
+
+ You may need to install python module: cProfile
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 2
+ min_args = 1
+
+ def _load_config_into_test_app(self):
+ from paste.deploy import loadapp
+ import paste.fixture
+ if not self.options.config:
+ msg = 'No config file supplied'
+ raise self.BadCommand(msg)
+ self.filename = os.path.abspath(self.options.config)
+ if not os.path.exists(self.filename):
+ raise AssertionError('Config filename %r does not exist.' % self.filename)
+ fileConfig(self.filename)
+
+ wsgiapp = loadapp('config:' + self.filename)
+ self.app = paste.fixture.TestApp(wsgiapp)
+
+ def command(self):
+ self._load_config_into_test_app()
+
+ import paste.fixture
+ import cProfile
+ import re
+
+ url = self.args[0]
+ if self.args[1:]:
+ user = self.args[1]
+ else:
+ user = 'visitor'
+
+ def profile_url(url):
+ try:
+ res = self.app.get(url, status=[200],
+ extra_environ={'REMOTE_USER': user})
+ except paste.fixture.AppError:
+ print('App error: ', url.strip())
+ except KeyboardInterrupt:
+ raise
+ except Exception:
+ error(traceback.format_exc())
+
+ output_filename = 'ckan%s.profile' % re.sub('[/?]', '.', url.replace('/', '.'))
+ profile_command = "profile_url('%s')" % url
+ cProfile.runctx(profile_command, globals(), locals(), filename=output_filename)
+ import pstats
+ stats = pstats.Stats(output_filename)
+ stats.sort_stats('cumulative')
+ stats.print_stats(0.1) # show only top 10% of lines
+ print('Only top 10% of lines shown')
+ print('Written profile to: %s' % output_filename)
+
+
+class CreateColorSchemeCommand(CkanCommand):
+ '''Create or remove a color scheme.
+
+ After running this, you'll need to regenerate the css files. See paster's less command for details.
+
+ color - creates a random color scheme
+ color clear - clears any color scheme
+ color <'HEX'> - uses as base color eg '#ff00ff' must be quoted.
+ color - a float between 0.0 and 1.0 used as base hue
+ color - html color name used for base color eg lightblue
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 1
+ min_args = 0
+
+ rules = [
+ '@layoutLinkColor',
+ '@mastheadBackgroundColor',
+ '@btnPrimaryBackground',
+ '@btnPrimaryBackgroundHighlight',
+ ]
+
+ # list of predefined colors
+ color_list = {
+ 'aliceblue': '#f0fff8',
+ 'antiquewhite': '#faebd7',
+ 'aqua': '#00ffff',
+ 'aquamarine': '#7fffd4',
+ 'azure': '#f0ffff',
+ 'beige': '#f5f5dc',
+ 'bisque': '#ffe4c4',
+ 'black': '#000000',
+ 'blanchedalmond': '#ffebcd',
+ 'blue': '#0000ff',
+ 'blueviolet': '#8a2be2',
+ 'brown': '#a52a2a',
+ 'burlywood': '#deb887',
+ 'cadetblue': '#5f9ea0',
+ 'chartreuse': '#7fff00',
+ 'chocolate': '#d2691e',
+ 'coral': '#ff7f50',
+ 'cornflowerblue': '#6495ed',
+ 'cornsilk': '#fff8dc',
+ 'crimson': '#dc143c',
+ 'cyan': '#00ffff',
+ 'darkblue': '#00008b',
+ 'darkcyan': '#008b8b',
+ 'darkgoldenrod': '#b8860b',
+ 'darkgray': '#a9a9a9',
+ 'darkgrey': '#a9a9a9',
+ 'darkgreen': '#006400',
+ 'darkkhaki': '#bdb76b',
+ 'darkmagenta': '#8b008b',
+ 'darkolivegreen': '#556b2f',
+ 'darkorange': '#ff8c00',
+ 'darkorchid': '#9932cc',
+ 'darkred': '#8b0000',
+ 'darksalmon': '#e9967a',
+ 'darkseagreen': '#8fbc8f',
+ 'darkslateblue': '#483d8b',
+ 'darkslategray': '#2f4f4f',
+ 'darkslategrey': '#2f4f4f',
+ 'darkturquoise': '#00ced1',
+ 'darkviolet': '#9400d3',
+ 'deeppink': '#ff1493',
+ 'deepskyblue': '#00bfff',
+ 'dimgray': '#696969',
+ 'dimgrey': '#696969',
+ 'dodgerblue': '#1e90ff',
+ 'firebrick': '#b22222',
+ 'floralwhite': '#fffaf0',
+ 'forestgreen': '#228b22',
+ 'fuchsia': '#ff00ff',
+ 'gainsboro': '#dcdcdc',
+ 'ghostwhite': '#f8f8ff',
+ 'gold': '#ffd700',
+ 'goldenrod': '#daa520',
+ 'gray': '#808080',
+ 'grey': '#808080',
+ 'green': '#008000',
+ 'greenyellow': '#adff2f',
+ 'honeydew': '#f0fff0',
+ 'hotpink': '#ff69b4',
+ 'indianred ': '#cd5c5c',
+ 'indigo ': '#4b0082',
+ 'ivory': '#fffff0',
+ 'khaki': '#f0e68c',
+ 'lavender': '#e6e6fa',
+ 'lavenderblush': '#fff0f5',
+ 'lawngreen': '#7cfc00',
+ 'lemonchiffon': '#fffacd',
+ 'lightblue': '#add8e6',
+ 'lightcoral': '#f08080',
+ 'lightcyan': '#e0ffff',
+ 'lightgoldenrodyellow': '#fafad2',
+ 'lightgray': '#d3d3d3',
+ 'lightgrey': '#d3d3d3',
+ 'lightgreen': '#90ee90',
+ 'lightpink': '#ffb6c1',
+ 'lightsalmon': '#ffa07a',
+ 'lightseagreen': '#20b2aa',
+ 'lightskyblue': '#87cefa',
+ 'lightslategray': '#778899',
+ 'lightslategrey': '#778899',
+ 'lightsteelblue': '#b0c4de',
+ 'lightyellow': '#ffffe0',
+ 'lime': '#00ff00',
+ 'limegreen': '#32cd32',
+ 'linen': '#faf0e6',
+ 'magenta': '#ff00ff',
+ 'maroon': '#800000',
+ 'mediumaquamarine': '#66cdaa',
+ 'mediumblue': '#0000cd',
+ 'mediumorchid': '#ba55d3',
+ 'mediumpurple': '#9370d8',
+ 'mediumseagreen': '#3cb371',
+ 'mediumslateblue': '#7b68ee',
+ 'mediumspringgreen': '#00fa9a',
+ 'mediumturquoise': '#48d1cc',
+ 'mediumvioletred': '#c71585',
+ 'midnightblue': '#191970',
+ 'mintcream': '#f5fffa',
+ 'mistyrose': '#ffe4e1',
+ 'moccasin': '#ffe4b5',
+ 'navajowhite': '#ffdead',
+ 'navy': '#000080',
+ 'oldlace': '#fdf5e6',
+ 'olive': '#808000',
+ 'olivedrab': '#6b8e23',
+ 'orange': '#ffa500',
+ 'orangered': '#ff4500',
+ 'orchid': '#da70d6',
+ 'palegoldenrod': '#eee8aa',
+ 'palegreen': '#98fb98',
+ 'paleturquoise': '#afeeee',
+ 'palevioletred': '#d87093',
+ 'papayawhip': '#ffefd5',
+ 'peachpuff': '#ffdab9',
+ 'peru': '#cd853f',
+ 'pink': '#ffc0cb',
+ 'plum': '#dda0dd',
+ 'powderblue': '#b0e0e6',
+ 'purple': '#800080',
+ 'red': '#ff0000',
+ 'rosybrown': '#bc8f8f',
+ 'royalblue': '#4169e1',
+ 'saddlebrown': '#8b4513',
+ 'salmon': '#fa8072',
+ 'sandybrown': '#f4a460',
+ 'seagreen': '#2e8b57',
+ 'seashell': '#fff5ee',
+ 'sienna': '#a0522d',
+ 'silver': '#c0c0c0',
+ 'skyblue': '#87ceeb',
+ 'slateblue': '#6a5acd',
+ 'slategray': '#708090',
+ 'slategrey': '#708090',
+ 'snow': '#fffafa',
+ 'springgreen': '#00ff7f',
+ 'steelblue': '#4682b4',
+ 'tan': '#d2b48c',
+ 'teal': '#008080',
+ 'thistle': '#d8bfd8',
+ 'tomato': '#ff6347',
+ 'turquoise': '#40e0d0',
+ 'violet': '#ee82ee',
+ 'wheat': '#f5deb3',
+ 'white': '#ffffff',
+ 'whitesmoke': '#f5f5f5',
+ 'yellow': '#ffff00',
+ 'yellowgreen': '#9acd32',
+ }
+
+ def create_colors(self, hue, num_colors=5, saturation=None, lightness=None):
+ if saturation is None:
+ saturation = 0.9
+ if lightness is None:
+ lightness = 40
+ else:
+ lightness *= 100
+
+ import math
+ saturation -= math.trunc(saturation)
+
+ print(hue, saturation)
+ import colorsys
+ ''' Create n related colours '''
+ colors = []
+ for i in xrange(num_colors):
+ ix = i * (1.0/num_colors)
+ _lightness = (lightness + (ix * 40))/100.
+ if _lightness > 1.0:
+ _lightness = 1.0
+ color = colorsys.hls_to_rgb(hue, _lightness, saturation)
+ hex_color = '#'
+ for part in color:
+ hex_color += '%02x' % int(part * 255)
+ # check and remove any bad values
+ if not re.match('^\#[0-9a-f]{6}$', hex_color):
+ hex_color = '#FFFFFF'
+ colors.append(hex_color)
+ return colors
+
+ def command(self):
+
+ hue = None
+ saturation = None
+ lightness = None
+
+ public = config.get(u'ckan.base_public_folder')
+ path = os.path.dirname(__file__)
+ path = os.path.join(path, '..', public, 'base', 'less', 'custom.less')
+
+ if self.args:
+ arg = self.args[0]
+ rgb = None
+ if arg == 'clear':
+ os.remove(path)
+ print('custom colors removed.')
+ elif arg.startswith('#'):
+ color = arg[1:]
+ if len(color) == 3:
+ rgb = [int(x, 16) * 16 for x in color]
+ elif len(color) == 6:
+ rgb = [int(x, 16) for x in re.findall('..', color)]
+ else:
+ print('ERROR: invalid color')
+ elif arg.lower() in self.color_list:
+ color = self.color_list[arg.lower()][1:]
+ rgb = [int(x, 16) for x in re.findall('..', color)]
+ else:
+ try:
+ hue = float(self.args[0])
+ except ValueError:
+ print('ERROR argument `%s` not recognised' % arg)
+ if rgb:
+ import colorsys
+ hue, lightness, saturation = colorsys.rgb_to_hls(*rgb)
+ lightness = lightness / 340
+ # deal with greys
+ if not (hue == 0.0 and saturation == 0.0):
+ saturation = None
+ else:
+ import random
+ hue = random.random()
+ if hue is not None:
+ f = open(path, 'w')
+ colors = self.create_colors(hue, saturation=saturation, lightness=lightness)
+ for i in xrange(len(self.rules)):
+ f.write('%s: %s;\n' % (self.rules[i], colors[i]))
+ print('%s: %s;\n' % (self.rules[i], colors[i]))
+ f.close
+ print('Color scheme has been created.')
+ print('Make sure less is run for changes to take effect.')
+
+
+class TranslationsCommand(CkanCommand):
+ '''Translation helper functions
+
+ trans js - generate the javascript translations
+ trans mangle - mangle the zh_TW translations for testing
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ max_args = 1
+ min_args = 1
+
+ def command(self):
+ self._load_config()
+ from ckan.common import config
+ from ckan.lib.i18n import build_js_translations
+ ckan_path = os.path.join(os.path.dirname(__file__), '..')
+ self.i18n_path = config.get('ckan.i18n_directory',
+ os.path.join(ckan_path, 'i18n'))
+ command = self.args[0]
+ if command == 'mangle':
+ self.mangle_po()
+ elif command == 'js':
+ build_js_translations()
+ else:
+ print('command not recognised')
+
+ def mangle_po(self):
+ ''' This will mangle the zh_TW translations for translation coverage
+ testing.
+
+ NOTE: This will destroy the current translations fot zh_TW
+ '''
+ import polib
+ pot_path = os.path.join(self.i18n_path, 'ckan.pot')
+ po = polib.pofile(pot_path)
+ # we don't want to mangle the following items in strings
+ # %(...)s %s %0.3f %1$s %2$0.3f [1:...] {...} etc
+
+ # sprintf bit after %
+ spf_reg_ex = "\+?(0|'.)?-?\d*(.\d*)?[\%bcdeufosxX]"
+
+ extract_reg_ex = '(\%\([^\)]*\)' + spf_reg_ex + \
+ '|\[\d*\:[^\]]*\]' + \
+ '|\{[^\}]*\}' + \
+ '|<[^>}]*>' + \
+ '|\%((\d)*\$)?' + spf_reg_ex + ')'
+
+ for entry in po:
+ msg = entry.msgid.encode('utf-8')
+ matches = re.finditer(extract_reg_ex, msg)
+ length = len(msg)
+ position = 0
+ translation = u''
+ for match in matches:
+ translation += '-' * (match.start() - position)
+ position = match.end()
+ translation += match.group(0)
+ translation += '-' * (length - position)
+ entry.msgstr = translation
+ out_dir = os.path.join(self.i18n_path, 'zh_TW', 'LC_MESSAGES')
+ try:
+ os.makedirs(out_dir)
+ except OSError:
+ pass
+ po.metadata['Plural-Forms'] = "nplurals=1; plural=0\n"
+ out_po = os.path.join(out_dir, 'ckan.po')
+ out_mo = os.path.join(out_dir, 'ckan.mo')
+ po.save(out_po)
+ po.save_as_mofile(out_mo)
+ print('zh_TW has been mangled')
+
+
+class MinifyCommand(CkanCommand):
+ '''Create minified versions of the given Javascript and CSS files.
+
+ Usage:
+
+ paster minify [--clean] PATH
+
+ for example:
+
+ paster minify ckan/public/base
+ paster minify ckan/public/base/css/*.css
+ paster minify ckan/public/base/css/red.css
+
+ if the --clean option is provided any minified files will be removed.
+
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ min_args = 1
+
+ exclude_dirs = ['vendor']
+
+ def __init__(self, name):
+
+ super(MinifyCommand, self).__init__(name)
+
+ self.parser.add_option('--clean', dest='clean',
+ action='store_true', default=False,
+ help='remove any minified files in the path')
+
+ def command(self):
+ clean = getattr(self.options, 'clean', False)
+ self._load_config()
+ for base_path in self.args:
+ if os.path.isfile(base_path):
+ if clean:
+ self.clear_minifyed(base_path)
+ else:
+ self.minify_file(base_path)
+ elif os.path.isdir(base_path):
+ for root, dirs, files in os.walk(base_path):
+ dirs[:] = [d for d in dirs if not d in self.exclude_dirs]
+ for filename in files:
+ path = os.path.join(root, filename)
+ if clean:
+ self.clear_minifyed(path)
+ else:
+ self.minify_file(path)
+ else:
+ # Path is neither a file or a dir?
+ continue
+
+ def clear_minifyed(self, path):
+ path_only, extension = os.path.splitext(path)
+
+ if extension not in ('.css', '.js'):
+ # This is not a js or css file.
+ return
+
+ if path_only.endswith('.min'):
+ print('removing %s' % path)
+ os.remove(path)
+
+ def minify_file(self, path):
+ '''Create the minified version of the given file.
+
+ If the file is not a .js or .css file (e.g. it's a .min.js or .min.css
+ file, or it's some other type of file entirely) it will not be
+ minifed.
+
+ :param path: The path to the .js or .css file to minify
+
+ '''
+ import ckan.lib.fanstatic_resources as fanstatic_resources
+
+ path_only, extension = os.path.splitext(path)
+
+ if path_only.endswith('.min'):
+ # This is already a minified file.
+ return
+
+ if extension not in ('.css', '.js'):
+ # This is not a js or css file.
+ return
+
+ path_min = fanstatic_resources.min_path(path)
+
+ source = open(path, 'r').read()
+ f = open(path_min, 'w')
+ if path.endswith('.css'):
+ f.write(rcssmin.cssmin(source))
+ elif path.endswith('.js'):
+ f.write(rjsmin.jsmin(source))
+ f.close()
+ print("Minified file '{0}'".format(path))
+
+
+class LessCommand(CkanCommand):
+ '''Compile all root less documents into their CSS counterparts
+
+ Usage:
+
+ paster less
+
+ '''
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ min_args = 0
+
+ def command(self):
+ self._load_config()
+ self.less()
+
+ custom_css = {
+ 'fuchsia': '''
+ @layoutLinkColor: #E73892;
+ @footerTextColor: mix(#FFF, @layoutLinkColor, 60%);
+ @footerLinkColor: @footerTextColor;
+ @mastheadBackgroundColor: @layoutLinkColor;
+ @btnPrimaryBackground: lighten(@layoutLinkColor, 10%);
+ @btnPrimaryBackgroundHighlight: @layoutLinkColor;
+ ''',
+
+ 'green': '''
+ @layoutLinkColor: #2F9B45;
+ @footerTextColor: mix(#FFF, @layoutLinkColor, 60%);
+ @footerLinkColor: @footerTextColor;
+ @mastheadBackgroundColor: @layoutLinkColor;
+ @btnPrimaryBackground: lighten(@layoutLinkColor, 10%);
+ @btnPrimaryBackgroundHighlight: @layoutLinkColor;
+ ''',
+
+ 'red': '''
+ @layoutLinkColor: #C14531;
+ @footerTextColor: mix(#FFF, @layoutLinkColor, 60%);
+ @footerLinkColor: @footerTextColor;
+ @mastheadBackgroundColor: @layoutLinkColor;
+ @btnPrimaryBackground: lighten(@layoutLinkColor, 10%);
+ @btnPrimaryBackgroundHighlight: @layoutLinkColor;
+ ''',
+
+ 'maroon': '''
+ @layoutLinkColor: #810606;
+ @footerTextColor: mix(#FFF, @layoutLinkColor, 60%);
+ @footerLinkColor: @footerTextColor;
+ @mastheadBackgroundColor: @layoutLinkColor;
+ @btnPrimaryBackground: lighten(@layoutLinkColor, 10%);
+ @btnPrimaryBackgroundHighlight: @layoutLinkColor;
+ ''',
+ }
+
+ def less(self):
+ ''' Compile less files '''
+ import subprocess
+ command = 'npm bin'
+ process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
+ output = process.communicate()
+ directory = output[0].strip()
+ less_bin = os.path.join(directory, 'lessc')
+
+ public = config.get(u'ckan.base_public_folder')
+
+ root = os.path.join(os.path.dirname(__file__), '..', public, 'base')
+ root = os.path.abspath(root)
+ custom_less = os.path.join(root, 'less', 'custom.less')
+ for color in self.custom_css:
+ f = open(custom_less, 'w')
+ f.write(self.custom_css[color])
+ f.close()
+ self.compile_less(root, less_bin, color)
+ f = open(custom_less, 'w')
+ f.write('// This file is needed in order for ./bin/less to compile in less 1.3.1+\n')
+ f.close()
+ self.compile_less(root, less_bin, 'main')
+
+ def compile_less(self, root, less_bin, color):
+ print('compile %s.css' % color)
+ import subprocess
+ main_less = os.path.join(root, 'less', 'main.less')
+ main_css = os.path.join(root, 'css', '%s.css' % color)
+
+ command = '%s %s %s' % (less_bin, main_less, main_css)
+
+ process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
+ output = process.communicate()
+ print(output)
+
+
+class FrontEndBuildCommand(CkanCommand):
+ '''Creates and minifies css and JavaScript files
+
+ Usage:
+
+ paster front-end-build
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ min_args = 0
+
+ def command(self):
+ self._load_config()
+
+ # Less css
+ cmd = LessCommand('less')
+ cmd.options = self.options
+ cmd.command()
+
+ # js translation strings
+ cmd = TranslationsCommand('trans')
+ cmd.options = self.options
+ cmd.args = ('js',)
+ cmd.command()
+
+ # minification
+ cmd = MinifyCommand('minify')
+ cmd.options = self.options
+ public = config.get(u'ckan.base_public_folder')
+ root = os.path.join(os.path.dirname(__file__), '..', public, 'base')
+ root = os.path.abspath(root)
+ ckanext = os.path.join(os.path.dirname(__file__), '..', '..', 'ckanext')
+ ckanext = os.path.abspath(ckanext)
+ cmd.args = (root, ckanext)
+ cmd.command()
+
+
+class ViewsCommand(CkanCommand):
+ '''Manage resource views.
+
+ Usage:
+
+ paster views create [options] [type1] [type2] ...
+
+ Create views on relevant resources. You can optionally provide
+ specific view types (eg `recline_view`, `image_view`). If no types
+ are provided, the default ones will be used. These are generally
+ the ones defined in the `ckan.views.default_views` config option.
+ Note that on either case, plugins must be loaded (ie added to
+ `ckan.plugins`), otherwise the command will stop.
+
+ paster views clear [options] [type1] [type2] ...
+
+ Permanently delete all views or the ones with the provided types.
+
+ paster views clean
+
+ Permanently delete views for all types no longer present in the
+ `ckan.plugins` configuration option.
+
+ '''
+
+ summary = __doc__.split('\n')[0]
+ usage = __doc__
+ min_args = 1
+
+ def __init__(self, name):
+
+ super(ViewsCommand, self).__init__(name)
+
+ self.parser.add_option('-y', '--yes', dest='assume_yes',
+ action='store_true',
+ default=False,
+ help='''Automatic yes to prompts. Assume "yes"
+as answer to all prompts and run non-interactively''')
+
+ self.parser.add_option('-d', '--dataset', dest='dataset_id',
+ action='append',
+ help='''Create views on a particular dataset.
+You can use the dataset id or name, and it can be defined multiple times.''')
+
+ self.parser.add_option('--no-default-filters',
+ dest='no_default_filters',
+ action='store_true',
+ default=False,
+ help='''Do not add default filters for relevant
+resource formats for the view types provided. Note that filters are not added
+by default anyway if an unsupported view type is provided or when using the
+`-s` or `-d` options.''')
+
+ self.parser.add_option('-s', '--search', dest='search_params',
+ action='store',
+ default=False,
+ help='''Extra search parameters that will be
+used for getting the datasets to create the resource views on. It must be a
+JSON object like the one used by the `package_search` API call. Supported
+fields are `q`, `fq` and `fq_list`. Check the documentation for examples.
+Not used when using the `-d` option.''')
+
+ def command(self):
+ self._load_config()
+ if not self.args:
+ print(self.usage)
+ elif self.args[0] == 'create':
+ view_plugin_types = self.args[1:]
+ self.create_views(view_plugin_types)
+ elif self.args[0] == 'clear':
+ view_plugin_types = self.args[1:]
+ self.clear_views(view_plugin_types)
+ elif self.args[0] == 'clean':
+ self.clean_views()
+ else:
+ print(self.usage)
+
+ _page_size = 100
+
+ def _get_view_plugins(self, view_plugin_types,
+ get_datastore_views=False):
+ '''
+ Returns the view plugins that were succesfully loaded
+
+ Views are provided as a list of ``view_plugin_types``. If no types are
+ provided, the default views defined in the ``ckan.views.default_views``
+ will be created. Only in this case (when the default view plugins are
+ used) the `get_datastore_views` parameter can be used to get also view
+ plugins that require data to be in the DataStore.
+
+ If any of the provided plugins could not be loaded (eg it was not added
+ to `ckan.plugins`) the command will stop.
+
+ Returns a list of loaded plugin names.
+ '''
+ from ckan.lib.datapreview import (get_view_plugins,
+ get_default_view_plugins
+ )
+
+ log = logging.getLogger(__name__)
+
+ view_plugins = []
+
+ if not view_plugin_types:
+ log.info('No view types provided, using default types')
+ view_plugins = get_default_view_plugins()
+ if get_datastore_views:
+ view_plugins.extend(
+ get_default_view_plugins(get_datastore_views=True))
+ else:
+ view_plugins = get_view_plugins(view_plugin_types)
+
+ loaded_view_plugins = [view_plugin.info()['name']
+ for view_plugin in view_plugins]
+
+ plugins_not_found = list(set(view_plugin_types) -
+ set(loaded_view_plugins))
+
+ if plugins_not_found:
+ error('View plugin(s) not found : {0}. '.format(plugins_not_found)
+ + 'Have they been added to the `ckan.plugins` configuration'
+ + ' option?')
+
+ return loaded_view_plugins
+
+ def _add_default_filters(self, search_data_dict, view_types):
+ '''
+ Adds extra filters to the `package_search` dict for common view types
+
+ It basically adds `fq` parameters that filter relevant resource formats
+ for the view types provided. For instance, if one of the view types is
+ `pdf_view` the following will be added to the final query:
+
+ fq=res_format:"pdf" OR res_format:"PDF"
+
+ This obviously should only be used if all view types are known and can
+ be filtered, otherwise we want all datasets to be returned. If a
+ non-filterable view type is provided, the search params are not
+ modified.
+
+ Returns the provided data_dict for `package_search`, optionally
+ modified with extra filters.
+ '''
+
+ from ckanext.imageview.plugin import DEFAULT_IMAGE_FORMATS
+ from ckanext.textview.plugin import get_formats as get_text_formats
+ from ckanext.datapusher.plugin import DEFAULT_FORMATS as \
+ datapusher_formats
+
+ filter_formats = []
+
+ for view_type in view_types:
+ if view_type == 'image_view':
+
+ for _format in DEFAULT_IMAGE_FORMATS:
+ filter_formats.extend([_format, _format.upper()])
+
+ elif view_type == 'text_view':
+ formats = get_text_formats(config)
+ for _format in itertools.chain.from_iterable(formats.values()):
+ filter_formats.extend([_format, _format.upper()])
+
+ elif view_type == 'pdf_view':
+ filter_formats.extend(['pdf', 'PDF'])
+
+ elif view_type in ['recline_view', 'recline_grid_view',
+ 'recline_graph_view', 'recline_map_view']:
+
+ if datapusher_formats[0] in filter_formats:
+ continue
+
+ for _format in datapusher_formats:
+ if '/' not in _format:
+ filter_formats.extend([_format, _format.upper()])
+ else:
+ # There is another view type provided so we can't add any
+ # filter
+ return search_data_dict
+
+ filter_formats_query = ['+res_format:"{0}"'.format(_format)
+ for _format in filter_formats]
+ search_data_dict['fq_list'].append(' OR '.join(filter_formats_query))
+
+ return search_data_dict
+
+ def _update_search_params(self, search_data_dict):
+ '''
+ Update the `package_search` data dict with the user provided parameters
+
+ Supported fields are `q`, `fq` and `fq_list`.
+
+ If the provided JSON object can not be parsed the process stops with
+ an error.
+
+ Returns the updated data dict
+ '''
+
+ log = logging.getLogger(__name__)
+
+ if not self.options.search_params:
+ return search_data_dict
+
+ try:
+ user_search_params = json.loads(self.options.search_params)
+ except ValueError as e:
+ error('Unable to parse JSON search parameters: {0}'.format(e))
+
+ if user_search_params.get('q'):
+ search_data_dict['q'] = user_search_params['q']
+
+ if user_search_params.get('fq'):
+ if search_data_dict['fq']:
+ search_data_dict['fq'] += ' ' + user_search_params['fq']
+ else:
+ search_data_dict['fq'] = user_search_params['fq']
+
+ if (user_search_params.get('fq_list') and
+ isinstance(user_search_params['fq_list'], list)):
+ search_data_dict['fq_list'].extend(user_search_params['fq_list'])
+
+ def _search_datasets(self, page=1, view_types=[]):
+ '''
+ Perform a query with `package_search` and return the result
+
+ Results can be paginated using the `page` parameter
+ '''
+
+ n = self._page_size
+
+ search_data_dict = {
+ 'q': '',
+ 'fq': '',
+ 'fq_list': [],
+ 'include_private': True,
+ 'rows': n,
+ 'start': n * (page - 1),
+ }
+
+ if self.options.dataset_id:
+
+ search_data_dict['q'] = ' OR '.join(
+ ['id:{0} OR name:"{0}"'.format(dataset_id)
+ for dataset_id in self.options.dataset_id]
+ )
+
+ elif self.options.search_params:
+
+ self._update_search_params(search_data_dict)
+
+ elif not self.options.no_default_filters:
+
+ self._add_default_filters(search_data_dict, view_types)
+
+ if not search_data_dict.get('q'):
+ search_data_dict['q'] = '*:*'
+
+ query = p.toolkit.get_action('package_search')(
+ {}, search_data_dict)
+
+ return query
+
+ def create_views(self, view_plugin_types=[]):
+
+ from ckan.lib.datapreview import add_views_to_dataset_resources
+
+ log = logging.getLogger(__name__)
+
+ datastore_enabled = 'datastore' in config['ckan.plugins'].split()
+
+ loaded_view_plugins = self._get_view_plugins(view_plugin_types,
+ datastore_enabled)
+
+ context = {'user': self.site_user['name']}
+
+ page = 1
+ while True:
+ query = self._search_datasets(page, loaded_view_plugins)
+
+ if page == 1 and query['count'] == 0:
+ error('No datasets to create resource views on, exiting...')
+
+ elif page == 1 and not self.options.assume_yes:
+
+ msg = ('\nYou are about to check {0} datasets for the ' +
+ 'following view plugins: {1}\n' +
+ ' Do you want to continue?')
+
+ confirm = query_yes_no(msg.format(query['count'],
+ loaded_view_plugins))
+
+ if confirm == 'no':
+ error('Command aborted by user')
+
+ if query['results']:
+ for dataset_dict in query['results']:
+
+ if not dataset_dict.get('resources'):
+ continue
+
+ views = add_views_to_dataset_resources(
+ context,
+ dataset_dict,
+ view_types=loaded_view_plugins)
+
+ if views:
+ view_types = list(set([view['view_type']
+ for view in views]))
+ msg = ('Added {0} view(s) of type(s) {1} to ' +
+ 'resources from dataset {2}')
+ log.debug(msg.format(len(views),
+ ', '.join(view_types),
+ dataset_dict['name']))
+
+ if len(query['results']) < self._page_size:
+ break
+
+ page += 1
+ else:
+ break
+
+ log.info('Done')
+
+ def clear_views(self, view_plugin_types=[]):
+
+ log = logging.getLogger(__name__)
+
+ if not self.options.assume_yes:
+ if view_plugin_types:
+ msg = 'Are you sure you want to delete all resource views ' + \
+ 'of type {0}?'.format(', '.join(view_plugin_types))
+ else:
+ msg = 'Are you sure you want to delete all resource views?'
+
+ result = query_yes_no(msg, default='no')
+
+ if result == 'no':
+ error('Command aborted by user')
+
+ context = {'user': self.site_user['name']}
+ logic.get_action('resource_view_clear')(
+ context, {'view_types': view_plugin_types})
+
+ log.info('Done')
+
+ def clean_views(self):
+ names = []
+ for plugin in p.PluginImplementations(p.IResourceView):
+ names.append(str(plugin.info()['name']))
+
+ results = model.ResourceView.get_count_not_in_view_types(names)
+
+ if not results:
+ print('No resource views to delete')
+ return
+
+ print('This command will delete.\n')
+ for row in results:
+ print('%s of type %s' % (row[1], row[0]))
+
+ result = query_yes_no('Do you want to delete these resource views:', default='no')
+
+ if result == 'no':
+ print('Not Deleting.')
+ return
+
+ model.ResourceView.delete_not_in_view_types(names)
+ model.Session.commit()
+ print('Deleted resource views.')
+
+
+class ConfigToolCommand(paste.script.command.Command):
+ '''Tool for editing options in a CKAN config file
+
+ paster config-tool = [= ...]
+ paster config-tool -f
+
+ Examples:
+ paster config-tool default.ini sqlalchemy.url=123 'ckan.site_title=ABC'
+ paster config-tool default.ini -s server:main -e port=8080
+ paster config-tool default.ini -f custom_options.ini
+ '''
+ parser = paste.script.command.Command.standard_parser(verbose=True)
+ default_verbosity = 1
+ group_name = 'ckan'
+ usage = __doc__
+ summary = usage.split('\n')[0]
+
+ parser.add_option('-s', '--section', dest='section',
+ default='app:main', help='Section of the config file')
+ parser.add_option(
+ '-e', '--edit', action='store_true', dest='edit', default=False,
+ help='Checks the option already exists in the config file')
+ parser.add_option(
+ '-f', '--file', dest='merge_filepath', metavar='FILE',
+ help='Supply an options file to merge in')
+
+ def command(self):
+ import config_tool
+ if len(self.args) < 1:
+ self.parser.error('Not enough arguments (got %i, need at least 1)'
+ % len(self.args))
+ config_filepath = self.args[0]
+ if not os.path.exists(config_filepath):
+ self.parser.error('Config filename %r does not exist.' %
+ config_filepath)
+ if self.options.merge_filepath:
+ config_tool.config_edit_using_merge_file(
+ config_filepath, self.options.merge_filepath)
+ options = self.args[1:]
+ if not (options or self.options.merge_filepath):
+ self.parser.error('No options provided')
+ if options:
+ for option in options:
+ if '=' not in option:
+ error(
+ 'An option does not have an equals sign: %r '
+ 'It should be \'key=value\'. If there are spaces '
+ 'you\'ll need to quote the option.\n' % option)
+ try:
+ config_tool.config_edit_using_option_strings(
+ config_filepath, options, self.options.section,
+ edit=self.options.edit)
+ except config_tool.ConfigToolError as e:
+ error(traceback.format_exc())
+
+
+class JobsCommand(CkanCommand):
+ '''Manage background jobs
+
+ Usage:
+
+ paster jobs worker [--burst] [QUEUES]
+
+ Start a worker that fetches jobs from queues and executes
+ them. If no queue names are given then the worker listens
+ to the default queue, this is equivalent to
+
+ paster jobs worker default
+
+ If queue names are given then the worker listens to those
+ queues and only those:
+
+ paster jobs worker my-custom-queue
+
+ Hence, if you want the worker to listen to the default queue
+ and some others then you must list the default queue explicitly:
+
+ paster jobs worker default my-custom-queue
+
+ If the `--burst` option is given then the worker will exit
+ as soon as all its queues are empty.
+
+ paster jobs list [QUEUES]
+
+ List currently enqueued jobs from the given queues. If no queue
+ names are given then the jobs from all queues are listed.
+
+ paster jobs show ID
+
+ Show details about a specific job.
+
+ paster jobs cancel ID
+
+ Cancel a specific job. Jobs can only be canceled while they are
+ enqueued. Once a worker has started executing a job it cannot
+ be aborted anymore.
+
+ paster jobs clear [QUEUES]
+
+ Cancel all jobs on the given queues. If no queue names are
+ given then ALL queues are cleared.
+
+ paster jobs test [QUEUES]
+
+ Enqueue a test job. If no queue names are given then the job is
+ added to the default queue. If queue names are given then a
+ separate test job is added to each of the queues.
+ '''
+
+ summary = __doc__.split(u'\n')[0]
+ usage = __doc__
+ min_args = 0
+
+
+ def __init__(self, *args, **kwargs):
+ super(JobsCommand, self).__init__(*args, **kwargs)
+ try:
+ self.parser.add_option(u'--burst', action='store_true',
+ default=False,
+ help=u'Start worker in burst mode.')
+ except OptionConflictError:
+ # Option has already been added in previous call
+ pass
+
+ def command(self):
+ self._load_config()
+ try:
+ cmd = self.args.pop(0)
+ except IndexError:
+ print(self.__doc__)
+ sys.exit(0)
+ if cmd == u'worker':
+ self.worker()
+ elif cmd == u'list':
+ self.list()
+ elif cmd == u'show':
+ self.show()
+ elif cmd == u'cancel':
+ self.cancel()
+ elif cmd == u'clear':
+ self.clear()
+ elif cmd == u'test':
+ self.test()
+ else:
+ error(u'Unknown command "{}"'.format(cmd))
+
+ def worker(self):
+ from ckan.lib.jobs import Worker
+ Worker(self.args).work(burst=self.options.burst)
+
+ def list(self):
+ data_dict = {
+ u'queues': self.args,
+ }
+ jobs = p.toolkit.get_action(u'job_list')({}, data_dict)
+ for job in jobs:
+ if job[u'title'] is None:
+ job[u'title'] = ''
+ else:
+ job[u'title'] = u'"{}"'.format(job[u'title'])
+ print(u'{created} {id} {queue} {title}'.format(**job))
+
+ def show(self):
+ if not self.args:
+ error(u'You must specify a job ID')
+ id = self.args[0]
+ try:
+ job = p.toolkit.get_action(u'job_show')({}, {u'id': id})
+ except logic.NotFound:
+ error(u'There is no job with ID "{}"'.format(id))
+ print(u'ID: {}'.format(job[u'id']))
+ if job[u'title'] is None:
+ title = u'None'
+ else:
+ title = u'"{}"'.format(job[u'title'])
+ print(u'Title: {}'.format(title))
+ print(u'Created: {}'.format(job[u'created']))
+ print(u'Queue: {}'.format(job[u'queue']))
+
+ def cancel(self):
+ if not self.args:
+ error(u'You must specify a job ID')
+ id = self.args[0]
+ try:
+ p.toolkit.get_action(u'job_cancel')({}, {u'id': id})
+ except logic.NotFound:
+ error(u'There is no job with ID "{}"'.format(id))
+ print(u'Cancelled job {}'.format(id))
+
+ def clear(self):
+ data_dict = {
+ u'queues': self.args,
+ }
+ queues = p.toolkit.get_action(u'job_clear')({}, data_dict)
+ queues = (u'"{}"'.format(q) for q in queues)
+ print(u'Cleared queue(s) {}'.format(u', '.join(queues)))
+
+ def test(self):
+ from ckan.lib.jobs import DEFAULT_QUEUE_NAME, enqueue, test_job
+ for queue in (self.args or [DEFAULT_QUEUE_NAME]):
+ job = enqueue(test_job, [u'A test job'], title=u'A test job', queue=queue)
+ print(u'Added test job {} to queue "{}"'.format(job.id, queue))
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/config_tool.py b/venv/lib/python2.7/site-packages/ckan/lib/config_tool.py
new file mode 100644
index 00000000..b90cda75
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/config_tool.py
@@ -0,0 +1,261 @@
+# encoding: utf-8
+
+import re
+
+INSERT_NEW_SECTIONS_BEFORE_SECTION = 'app:main'
+
+
+def config_edit_using_option_strings(config_filepath, desired_option_strings,
+ section, edit=False):
+ '''Writes the desired_option_strings to the config file.'''
+ # Parse the desired_options
+ desired_options = [parse_option_string(section, desired_option_string,
+ raise_on_error=True)
+ for desired_option_string in desired_option_strings]
+ # Make the changes
+ config_edit(config_filepath, desired_options, edit=edit)
+
+
+def config_edit_using_merge_file(config_filepath, merge_config_filepath):
+ '''Merges options found in a config file (merge_config_filepath) into the
+ main config file (config_filepath).
+ '''
+ # Read and parse the merge config filepath
+ with open(merge_config_filepath, 'rb') as f:
+ input_lines = [line.rstrip('\n') for line in f]
+ desired_options_dict = parse_config(input_lines)
+ desired_options = desired_options_dict.values()
+ # Make the changes
+ config_edit(config_filepath, desired_options)
+
+
+def config_edit(config_filepath, desired_options, edit=False):
+ '''Writes the desired_options to the config file.'''
+ # Read and parse the existing config file
+ with open(config_filepath, 'rb') as f:
+ input_lines = [line.rstrip('\n') for line in f]
+ existing_options_dict = parse_config(input_lines)
+ existing_options = existing_options_dict.values()
+
+ # For every desired option, decide what action to take
+ new_sections = calculate_new_sections(existing_options, desired_options)
+ changes = calculate_changes(existing_options_dict, desired_options, edit)
+
+ # write the file with the changes
+ output = make_changes(input_lines, new_sections, changes)
+ with open(config_filepath, 'wb') as f:
+ f.write('\n'.join(output) + '\n')
+
+
+def parse_option_string(section, option_string, raise_on_error=False):
+ option_match = OPTION_RE.match(option_string)
+ if not option_match:
+ if raise_on_error:
+ raise ConfigToolError('Option did not parse: "%s". Must be: '
+ '"key = value"' % option_string)
+ return
+ is_commented_out, key, value = option_match.group('commentedout',
+ 'option', 'value')
+ key = key.strip()
+ value = value.strip()
+ return Option(section, key, value, is_commented_out,
+ original=option_string)
+
+
+class Option(object):
+ def __init__(self, section, key, value, is_commented_out, original=None):
+ self.section = section
+ self.key = key
+ self.value = value
+ self.is_commented_out = bool(is_commented_out)
+ self.original = original
+
+ def __repr__(self):
+ return '' % (self.section, self)
+
+ def __str__(self):
+ if self.original:
+ return self.original
+ return '%s%s = %s' % ('#' if self.is_commented_out else '',
+ self.key, self.value)
+
+ @property
+ def id(self):
+ return '%s-%s' % (self.section, self.key)
+
+ def comment_out(self):
+ self.is_commented_out = True
+ self.original = None # it is no longer accurate
+
+
+def calculate_new_sections(existing_options, desired_options):
+ existing_sections = set([option.section for option in existing_options])
+ desired_sections = set([option.section for option in desired_options])
+ new_sections = desired_sections - existing_sections
+ return new_sections
+
+
+class Changes(dict):
+ '''A store of Options that are to "edit" or "add" to existing sections of a
+ config file. (Excludes options that go into new sections.)'''
+ def add(self, action, option):
+ assert action in ('edit', 'add')
+ assert isinstance(option, Option)
+ if option.section not in self:
+ self[option.section] = {}
+ if not self[option.section].get(action):
+ self[option.section][action] = []
+ self[option.section][action].append(option)
+
+ def get(self, section, action):
+ try:
+ return self[section][action]
+ except KeyError:
+ return []
+
+
+def calculate_changes(existing_options_dict, desired_options, edit):
+ changes = Changes()
+
+ for desired_option in desired_options:
+ action = 'edit' if desired_option.id in existing_options_dict \
+ else 'add'
+ if edit and action != 'edit':
+ raise ConfigToolError(
+ 'Key "%s" does not exist in section "%s"' %
+ (desired_option.key, desired_option.section))
+ changes.add(action, desired_option)
+ return changes
+
+
+def parse_config(input_lines):
+ '''
+ Returns a dict of Option objects, keyed by Option.id, given the lines in a
+ config file.
+ (Not using ConfigParser.set() as it does not store all the comments and
+ ordering)
+ '''
+ section = 'app:main' # default (for merge config files)
+ options = {}
+ for line in input_lines:
+ # ignore blank lines
+ if line.strip() == '':
+ continue
+ # section heading
+ section_match = SECTION_RE.match(line)
+ if section_match:
+ section = section_match.group('header')
+ continue
+ # option
+ option = parse_option_string(section, line)
+ if option:
+ options[option.id] = option
+ return options
+
+
+def make_changes(input_lines, new_sections, changes):
+ '''Makes changes to the config file (returned as lines).'''
+ output = []
+ section = None
+ options_to_edit_in_this_section = {} # key: option
+ options_already_edited = set()
+ have_inserted_new_sections = False
+
+ def write_option(option):
+ output.append(str(option))
+
+ def insert_new_sections(new_sections):
+ for section in new_sections:
+ output.append('[%s]' % section)
+ for option in changes.get(section, 'add'):
+ write_option(option)
+ write_option('')
+ print('Created option %s = "%s" (NEW section "%s")' %
+ (option.key, option.value, section))
+
+ for line in input_lines:
+ # leave blank lines alone
+ if line.strip() == '':
+ output.append(line)
+ continue
+ section_match = SECTION_RE.match(line)
+ if section_match:
+ section = section_match.group('header')
+ if section == INSERT_NEW_SECTIONS_BEFORE_SECTION:
+ # insert new sections here
+ insert_new_sections(new_sections)
+ have_inserted_new_sections = True
+ output.append(line)
+ # at start of new section, write the 'add'ed options
+ for option in changes.get(section, 'add'):
+ write_option(option)
+ options_to_edit_in_this_section = {option.key: option
+ for option
+ in changes.get(section, 'edit')}
+ continue
+ existing_option = parse_option_string(section, line)
+ if not existing_option:
+ # leave alone comments (does not include commented options)
+ output.append(line)
+ continue
+ updated_option = \
+ options_to_edit_in_this_section.get(existing_option.key)
+ if updated_option:
+ changes_made = None
+ key = existing_option.key
+ if existing_option.id in options_already_edited:
+ if not existing_option.is_commented_out:
+ print('Commented out repeat of %s (section "%s")' %
+ (key, section))
+ existing_option.comment_out()
+ else:
+ print('Left commented out repeat of %s (section "%s")' %
+ (key, section))
+ elif not existing_option.is_commented_out and \
+ updated_option.is_commented_out:
+ changes_made = 'Commented out %s (section "%s")' % \
+ (key, section)
+ elif existing_option.is_commented_out and \
+ not updated_option.is_commented_out:
+ changes_made = 'Option uncommented and set %s = "%s" ' \
+ '(section "%s")' % \
+ (key, updated_option.value, section)
+ elif not existing_option.is_commented_out and \
+ not updated_option.is_commented_out:
+ if existing_option.value != updated_option.value:
+ changes_made = 'Edited option %s = "%s"->"%s" ' \
+ '(section "%s")' % \
+ (key, existing_option.value,
+ updated_option.value, section)
+ else:
+ changes_made = 'Option unchanged %s = "%s" ' \
+ '(section "%s")' % \
+ (key, existing_option.value, section)
+
+ if changes_made:
+ print(changes_made)
+ write_option(updated_option)
+ options_already_edited.add(updated_option.id)
+ else:
+ write_option(existing_option)
+ else:
+ write_option(existing_option)
+ if new_sections and not have_inserted_new_sections:
+ # must not have found the INSERT_NEW_SECTIONS_BEFORE_SECTION
+ # section so put the new sections at the end
+ insert_new_sections(new_sections)
+
+ return output
+
+
+# Regexes basically the same as in ConfigParser - OPTCRE & SECTCRE
+# Expressing them here because they move between Python 2 and 3
+OPTION_RE = re.compile(r'(?P[#;]\s*)?' # custom
+ r'(?P[^:=\s][^:=]*)'
+ r'\s*(?P[:=])\s*'
+ r'(?P.*)$')
+SECTION_RE = re.compile(r'\[(?P.+)\]')
+
+
+class ConfigToolError(Exception):
+ pass
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/create_test_data.py b/venv/lib/python2.7/site-packages/ckan/lib/create_test_data.py
new file mode 100644
index 00000000..f6929c6e
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/create_test_data.py
@@ -0,0 +1,933 @@
+# encoding: utf-8
+
+import logging
+from collections import defaultdict
+import datetime
+
+from six import string_types, text_type
+
+import ckan.model as model
+
+log = logging.getLogger(__name__)
+
+class CreateTestData(object):
+ # keep track of the objects created by this class so that
+ # tests can easy call delete() method to delete them all again.
+ pkg_names = []
+ tag_names = []
+ group_names = set()
+ user_refs = []
+
+ author = u'tester'
+
+ pkg_core_fields = ['name', 'title', 'version', 'url', 'notes',
+ 'author', 'author_email',
+ 'maintainer', 'maintainer_email',
+ 'private',
+ ]
+ @classmethod
+ def create_basic_test_data(cls):
+ cls.create()
+
+ @classmethod
+ def create_search_test_data(cls):
+ cls.create_arbitrary(search_items)
+
+ @classmethod
+ def create_gov_test_data(cls, extra_users=[]):
+ cls.create_arbitrary(gov_items, extra_user_names=extra_users)
+
+ @classmethod
+ def create_family_test_data(cls, extra_users=[]):
+ cls.create_arbitrary(family_items,
+ relationships=family_relationships,
+ extra_user_names=extra_users)
+
+ @classmethod
+ def create_group_hierarchy_test_data(cls, extra_users=[]):
+ cls.create_users(group_hierarchy_users)
+ cls.create_groups(group_hierarchy_groups)
+ cls.create_arbitrary(group_hierarchy_datasets)
+
+ @classmethod
+ def create_test_user(cls):
+ tester = model.User.by_name(u'tester')
+ if tester is None:
+ tester = model.User(name=u'tester', apikey=u'tester',
+ password=u'tester')
+ model.Session.add(tester)
+ model.Session.commit()
+ model.Session.remove()
+ cls.user_refs.append(u'tester')
+
+ @classmethod
+
+ def create_translations_test_data(cls):
+ import ckan.model
+ CreateTestData.create()
+ rev = ckan.model.repo.new_revision()
+ rev.author = CreateTestData.author
+ rev.message = u'Creating test translations.'
+
+ sysadmin_user = ckan.model.User.get('testsysadmin')
+ package = ckan.model.Package.get('annakarenina')
+
+ # Add some new tags to the package.
+ # These tags are codes that are meant to be always translated before
+ # display, if not into the user's current language then into the
+ # fallback language.
+ package.add_tags([ckan.model.Tag('123'), ckan.model.Tag('456'),
+ ckan.model.Tag('789')])
+
+ # Add the above translations to CKAN.
+ for (lang_code, translations) in (('de', german_translations),
+ ('fr', french_translations), ('en', english_translations)):
+ for term in terms:
+ if term in translations:
+ data_dict = {
+ 'term': term,
+ 'term_translation': translations[term],
+ 'lang_code': lang_code,
+ }
+ context = {
+ 'model': ckan.model,
+ 'session': ckan.model.Session,
+ 'user': sysadmin_user.name,
+ }
+ ckan.logic.action.update.term_translation_update(context,
+ data_dict)
+
+ ckan.model.Session.commit()
+
+ def create_vocabs_test_data(cls):
+ import ckan.model
+ CreateTestData.create()
+ sysadmin_user = ckan.model.User.get('testsysadmin')
+ annakarenina = ckan.model.Package.get('annakarenina')
+ warandpeace = ckan.model.Package.get('warandpeace')
+
+ # Create a couple of vocabularies.
+ context = {
+ 'model': ckan.model,
+ 'session': ckan.model.Session,
+ 'user': sysadmin_user.name
+ }
+ data_dict = {
+ 'name': 'Genre',
+ 'tags': [{'name': 'Drama'}, {'name': 'Sci-Fi'},
+ {'name': 'Mystery'}],
+ }
+ ckan.logic.action.create.vocabulary_create(context, data_dict)
+
+ data_dict = {
+ 'name': 'Actors',
+ 'tags': [{'name': 'keira-knightley'}, {'name': 'jude-law'},
+ {'name': 'alessio-boni'}],
+ }
+ ckan.logic.action.create.vocabulary_create(context, data_dict)
+
+ # Add some vocab tags to some packages.
+ genre_vocab = ckan.model.Vocabulary.get('Genre')
+ actors_vocab = ckan.model.Vocabulary.get('Actors')
+ annakarenina.add_tag_by_name('Drama', vocab=genre_vocab)
+ annakarenina.add_tag_by_name('keira-knightley', vocab=actors_vocab)
+ annakarenina.add_tag_by_name('jude-law', vocab=actors_vocab)
+ warandpeace.add_tag_by_name('Drama', vocab=genre_vocab)
+ warandpeace.add_tag_by_name('alessio-boni', vocab=actors_vocab)
+
+ @classmethod
+ def create_arbitrary(cls, package_dicts, relationships=[],
+ extra_user_names=[], extra_group_names=[]):
+ '''Creates packages and a few extra objects as well at the
+ same time if required.
+ @param package_dicts - a list of dictionaries with the package
+ properties.
+ Extra keys allowed:
+ @param extra_group_names - a list of group names to create. No
+ properties get set though.
+ '''
+ assert isinstance(relationships, (list, tuple))
+ assert isinstance(extra_user_names, (list, tuple))
+ assert isinstance(extra_group_names, (list, tuple))
+ model.Session.remove()
+ new_user_names = extra_user_names
+ new_group_names = set()
+ new_groups = {}
+
+ if package_dicts:
+ if isinstance(package_dicts, dict):
+ package_dicts = [package_dicts]
+ for item in package_dicts:
+ rev = model.repo.new_revision()
+ rev.author = cls.author
+ rev.message = u'Creating test packages.'
+ pkg_dict = {}
+ for field in cls.pkg_core_fields:
+ if item.has_key(field):
+ pkg_dict[field] = text_type(item[field])
+ if model.Package.by_name(pkg_dict['name']):
+ log.warning('Cannot create package "%s" as it already exists.' % \
+ (pkg_dict['name']))
+ continue
+ pkg = model.Package(**pkg_dict)
+ model.Session.add(pkg)
+ for attr, val in item.items():
+ if isinstance(val, str):
+ val = text_type(val)
+ if attr=='name':
+ continue
+ if attr in cls.pkg_core_fields:
+ pass
+ elif attr == 'download_url':
+ pkg.add_resource(text_type(val))
+ elif attr == 'resources':
+ assert isinstance(val, (list, tuple))
+ for res_dict in val:
+ non_extras = {}
+ for k, v in res_dict.items():
+ if k != 'extras':
+ if not isinstance(v, datetime.datetime):
+ v = text_type(v)
+ non_extras[str(k)] = v
+ extras = {str(k): text_type(v) for k, v in res_dict.get('extras', {}).items()}
+ pkg.add_resource(extras=extras, **non_extras)
+ elif attr == 'tags':
+ if isinstance(val, string_types):
+ tags = val.split()
+ elif isinstance(val, list):
+ tags = val
+ else:
+ raise NotImplementedError
+ for tag_name in tags:
+ tag_name = text_type(tag_name)
+ tag = model.Tag.by_name(tag_name)
+ if not tag:
+ tag = model.Tag(name=tag_name)
+ cls.tag_names.append(tag_name)
+ model.Session.add(tag)
+ pkg.add_tag(tag)
+ model.Session.flush()
+ elif attr == 'groups':
+ model.Session.flush()
+ if isinstance(val, string_types):
+ group_names = val.split()
+ elif isinstance(val, list):
+ group_names = val
+ else:
+ raise NotImplementedError
+ for group_name in group_names:
+ group = model.Group.by_name(text_type(group_name))
+ if not group:
+ if not group_name in new_groups:
+ group = model.Group(name=
+ text_type(group_name))
+ model.Session.add(group)
+ new_group_names.add(group_name)
+ new_groups[group_name] = group
+ else:
+ # If adding multiple packages with the same
+ # group name, model.Group.by_name will not
+ # find the group as the session has not yet
+ # been committed at this point. Fetch from
+ # the new_groups dict instead.
+ group = new_groups[group_name]
+ capacity = 'organization' if group.is_organization\
+ else 'public'
+ member = model.Member(group=group, table_id=pkg.id,
+ table_name='package',
+ capacity=capacity)
+ model.Session.add(member)
+ if group.is_organization:
+ pkg.owner_org = group.id
+ elif attr == 'license':
+ pkg.license_id = val
+ elif attr == 'license_id':
+ pkg.license_id = val
+ elif attr == 'extras':
+ pkg.extras = val
+ elif attr == 'admins':
+ assert 0, 'Deprecated param "admins"'
+ else:
+ raise NotImplementedError(attr)
+ cls.pkg_names.append(item['name'])
+ model.repo.commit_and_remove()
+
+ needs_commit = False
+
+ rev = model.repo.new_revision()
+ for group_name in extra_group_names:
+ group = model.Group(name=text_type(group_name))
+ model.Session.add(group)
+ new_group_names.add(group_name)
+ needs_commit = True
+
+ if needs_commit:
+ model.repo.commit_and_remove()
+ needs_commit = False
+
+ # create users that have been identified as being needed
+ for user_name in new_user_names:
+ if not model.User.by_name(text_type(user_name)):
+ user = model.User(name=text_type(user_name))
+ model.Session.add(user)
+ cls.user_refs.append(user_name)
+ needs_commit = True
+
+ if needs_commit:
+ model.repo.commit_and_remove()
+ needs_commit = False
+
+ # setup authz for groups just created
+ for group_name in new_group_names:
+ group = model.Group.by_name(text_type(group_name))
+ cls.group_names.add(group_name)
+ needs_commit = True
+
+ if needs_commit:
+ model.repo.commit_and_remove()
+ needs_commit = False
+
+ if relationships:
+ rev = model.repo.new_revision()
+ rev.author = cls.author
+ rev.message = u'Creating package relationships.'
+
+ def pkg(pkg_name):
+ return model.Package.by_name(text_type(pkg_name))
+ for subject_name, relationship, object_name in relationships:
+ pkg(subject_name).add_relationship(
+ text_type(relationship), pkg(object_name))
+ needs_commit = True
+
+ model.repo.commit_and_remove()
+
+
+ @classmethod
+ def create_groups(cls, group_dicts, admin_user_name=None, auth_profile=""):
+ '''A more featured interface for creating groups.
+ All group fields can be filled, packages added, can have
+ an admin user and be a member of other groups.'''
+ rev = model.repo.new_revision()
+ rev.author = cls.author
+ if admin_user_name:
+ admin_users = [model.User.by_name(admin_user_name)]
+ else:
+ admin_users = []
+ assert isinstance(group_dicts, (list, tuple))
+ group_attributes = set(('name', 'title', 'description', 'parent_id',
+ 'type', 'is_organization'))
+ for group_dict in group_dicts:
+ if model.Group.by_name(text_type(group_dict['name'])):
+ log.warning('Cannot create group "%s" as it already exists.' %
+ group_dict['name'])
+ continue
+ pkg_names = group_dict.pop('packages', [])
+ group = model.Group(name=text_type(group_dict['name']))
+ group.type = auth_profile or 'group'
+ for key in group_dict:
+ if key in group_attributes:
+ setattr(group, key, group_dict[key])
+ elif key not in ('admins', 'editors', 'parent'):
+ group.extras[key] = group_dict[key]
+ assert isinstance(pkg_names, (list, tuple))
+ for pkg_name in pkg_names:
+ pkg = model.Package.by_name(text_type(pkg_name))
+ assert pkg, pkg_name
+ member = model.Member(group=group, table_id=pkg.id,
+ table_name='package')
+ model.Session.add(member)
+ model.Session.add(group)
+ admins = [model.User.by_name(user_name)
+ for user_name in group_dict.get('admins', [])] + \
+ admin_users
+ for admin in admins:
+ member = model.Member(group=group, table_id=admin.id,
+ table_name='user', capacity='admin')
+ model.Session.add(member)
+ editors = [model.User.by_name(user_name)
+ for user_name in group_dict.get('editors', [])]
+ for editor in editors:
+ member = model.Member(group=group, table_id=editor.id,
+ table_name='user', capacity='editor')
+ model.Session.add(member)
+ # Need to commit the current Group for two reasons:
+ # 1. It might have a parent, and the Member will need the Group.id
+ # value allocated on commit.
+ # 2. The next Group created may have this Group as a parent so
+ # creation of the Member needs to refer to this one.
+ model.Session.commit()
+ rev = model.repo.new_revision()
+ rev.author = cls.author
+ # add it to a parent's group
+ if 'parent' in group_dict:
+ parent = model.Group.by_name(text_type(group_dict['parent']))
+ assert parent, group_dict['parent']
+ member = model.Member(group=group, table_id=parent.id,
+ table_name='group', capacity='parent')
+ model.Session.add(member)
+ cls.group_names.add(group_dict['name'])
+ model.repo.commit_and_remove()
+
+ @classmethod
+ def create(cls, auth_profile="", package_type=None):
+ model.Session.remove()
+ rev = model.repo.new_revision()
+ # same name as user we create below
+ rev.author = cls.author
+ rev.message = u'''Creating test data.
+ * Package: annakarenina
+ * Package: warandpeace
+ * Associated tags, etc etc
+'''
+ if auth_profile == "publisher":
+ organization_group = model.Group(name=u"organization_group",
+ type="organization")
+
+ cls.pkg_names = [u'annakarenina', u'warandpeace']
+ pkg1 = model.Package(name=cls.pkg_names[0], type=package_type)
+ if auth_profile == "publisher":
+ pkg1.group = organization_group
+ model.Session.add(pkg1)
+ pkg1.title = u'A Novel By Tolstoy'
+ pkg1.version = u'0.7a'
+ pkg1.url = u'http://datahub.io'
+ # put an & in the url string to test escaping
+ if 'alt_url' in model.Resource.get_extra_columns():
+ configured_extras = ({'alt_url': u'alt123'},
+ {'alt_url': u'alt345'})
+ else:
+ configured_extras = ({}, {})
+ pr1 = model.Resource(
+ url=u'http://datahub.io/download/x=1&y=2',
+ format=u'plain text',
+ description=u'Full text. Needs escaping: " Umlaut: \xfc',
+ hash=u'abc123',
+ extras={'size_extra': u'123'},
+ **configured_extras[0]
+ )
+ pr2 = model.Resource(
+ url=u'http://datahub.io/index.json',
+ format=u'JSON',
+ description=u'Index of the novel',
+ hash=u'def456',
+ extras={'size_extra': u'345'},
+ **configured_extras[1]
+ )
+ model.Session.add(pr1)
+ model.Session.add(pr2)
+ pkg1.resources_all.append(pr1)
+ pkg1.resources_all.append(pr2)
+ pkg1.notes = u'''Some test notes
+
+### A 3rd level heading
+
+**Some bolded text.**
+
+*Some italicized text.*
+
+Foreign characters:
+u with umlaut \xfc
+66-style quote \u201c
+foreign word: th\xfcmb
+
+Needs escaping:
+left arrow <
+
+
+
+'''
+ pkg2 = model.Package(name=cls.pkg_names[1], type=package_type)
+ tag1 = model.Tag(name=u'russian')
+ tag2 = model.Tag(name=u'tolstoy')
+
+ if auth_profile == "publisher":
+ pkg2.group = organization_group
+
+ # Flexible tag, allows spaces, upper-case,
+ # and all punctuation except commas
+ tag3 = model.Tag(name=u'Flexible \u30a1')
+
+ for obj in [pkg2, tag1, tag2, tag3]:
+ model.Session.add(obj)
+ pkg1.add_tags([tag1, tag2, tag3])
+ pkg2.add_tags([ tag1, tag3 ])
+ cls.tag_names = [ t.name for t in (tag1, tag2, tag3) ]
+ pkg1.license_id = u'other-open'
+ pkg2.license_id = u'cc-nc' # closed license
+ pkg2.title = u'A Wonderful Story'
+ pkg1.extras = {u'genre':'romantic novel',
+ u'original media':'book'}
+ # group
+ david = model.Group(name=u'david',
+ title=u'Dave\'s books',
+ description=u'These are books that David likes.',
+ type=auth_profile or 'group')
+ roger = model.Group(name=u'roger',
+ title=u'Roger\'s books',
+ description=u'Roger likes these books.',
+ type=auth_profile or 'group')
+
+ for obj in [david, roger]:
+ model.Session.add(obj)
+
+ cls.group_names.add(u'david')
+ cls.group_names.add(u'roger')
+
+ model.Session.flush()
+
+ model.Session.add(model.Member(table_id=pkg1.id, table_name='package', group=david))
+ model.Session.add(model.Member(table_id=pkg2.id, table_name='package', group=david))
+ model.Session.add(model.Member(table_id=pkg1.id, table_name='package', group=roger))
+ # authz
+ sysadmin = model.User(name=u'testsysadmin', password=u'testsysadmin')
+ sysadmin.sysadmin = True
+ model.Session.add_all([
+ model.User(name=u'tester', apikey=u'tester', password=u'tester'),
+ model.User(name=u'joeadmin', password=u'joeadmin'),
+ model.User(name=u'annafan', about=u'I love reading Annakarenina. My site: http://datahub.io', password=u'annafan'),
+ model.User(name=u'russianfan', password=u'russianfan'),
+ sysadmin,
+ ])
+ cls.user_refs.extend([u'tester', u'joeadmin', u'annafan', u'russianfan', u'testsysadmin'])
+ model.repo.commit_and_remove()
+
+ # method used in DGU and all good tests elsewhere
+ @classmethod
+ def create_users(cls, user_dicts):
+ needs_commit = False
+ for user_dict in user_dicts:
+ user = cls._create_user_without_commit(**user_dict)
+ if user:
+ needs_commit = True
+ if needs_commit:
+ model.repo.commit_and_remove()
+
+ @classmethod
+ def _create_user_without_commit(cls, name='', **user_dict):
+ if model.User.by_name(name):
+ log.warning('Cannot create user "%s" as it already exists.' %
+ name or user_dict['name'])
+ return
+ # User objects are not revisioned so no need to create a revision
+ user_ref = name
+ assert user_ref
+ for k, v in user_dict.items():
+ if v:
+ # avoid unicode warnings
+ user_dict[k] = text_type(v)
+ user = model.User(name=text_type(name), **user_dict)
+ model.Session.add(user)
+ cls.user_refs.append(user_ref)
+ return user
+
+ @classmethod
+ def create_user(cls, name='', **kwargs):
+ user = cls._create_user_without_commit(name, **kwargs)
+ model.Session.commit()
+ return user
+
+ @classmethod
+ def flag_for_deletion(cls, pkg_names=[], tag_names=[], group_names=[],
+ user_names=[]):
+ '''If you create a domain object manually in your test then you
+ can name it here (flag it up) and it will be deleted when you next
+ call CreateTestData.delete().'''
+ if isinstance(pkg_names, string_types):
+ pkg_names = [pkg_names]
+ cls.pkg_names.extend(pkg_names)
+ cls.tag_names.extend(tag_names)
+ cls.group_names = cls.group_names.union(set(group_names))
+ cls.user_refs.extend(user_names)
+
+ @classmethod
+ def delete(cls):
+ '''Purges packages etc. that were created by this class.'''
+ for pkg_name in cls.pkg_names:
+ model.Session().autoflush = False
+ pkg = model.Package.by_name(text_type(pkg_name))
+ if pkg:
+ pkg.purge()
+ for tag_name in cls.tag_names:
+ tag = model.Tag.by_name(text_type(tag_name))
+ if tag:
+ tag.purge()
+ for group_name in cls.group_names:
+ group = model.Group.by_name(text_type(group_name))
+ if group:
+ model.Session.delete(group)
+ revs = model.Session.query(model.Revision).filter_by(author=cls.author)
+ for rev in revs:
+ for pkg in rev.packages:
+ pkg.purge()
+ for grp in rev.groups:
+ grp.purge()
+ model.Session.commit()
+ model.Session.delete(rev)
+ for user_name in cls.user_refs:
+ user = model.User.get(text_type(user_name))
+ if user:
+ user.purge()
+ model.Session.commit()
+ model.Session.remove()
+ cls.reset()
+
+ @classmethod
+ def reset(cls):
+ cls.pkg_names = []
+ cls.group_names = set()
+ cls.tag_names = []
+ cls.user_refs = []
+
+ @classmethod
+ def get_all_data(cls):
+ return cls.pkg_names + list(cls.group_names) + cls.tag_names + cls.user_refs
+
+ @classmethod
+ def make_some_vocab_tags(cls):
+ model.repo.new_revision()
+
+ # Create a couple of vocabularies.
+ genre_vocab = model.Vocabulary(u'genre')
+ model.Session.add(genre_vocab)
+ composers_vocab = model.Vocabulary(u'composers')
+ model.Session.add(composers_vocab)
+
+ # Create some additional free tags for tag search tests.
+ tolkien_tag = model.Tag(name="tolkien")
+ model.Session.add(tolkien_tag)
+ toledo_tag = model.Tag(name="toledo")
+ model.Session.add(toledo_tag)
+ tolerance_tag = model.Tag(name="tolerance")
+ model.Session.add(tolerance_tag)
+ tollbooth_tag = model.Tag(name="tollbooth")
+ model.Session.add(tollbooth_tag)
+ # We have to add free tags to a package or they won't show up in tag results.
+ model.Package.get('warandpeace').add_tags((tolkien_tag, toledo_tag,
+ tolerance_tag, tollbooth_tag))
+
+ # Create some tags that belong to vocabularies.
+ sonata_tag = model.Tag(name=u'sonata', vocabulary_id=genre_vocab.id)
+ model.Session.add(sonata_tag)
+
+ bach_tag = model.Tag(name=u'Bach', vocabulary_id=composers_vocab.id)
+ model.Session.add(bach_tag)
+
+ neoclassical_tag = model.Tag(name='neoclassical',
+ vocabulary_id=genre_vocab.id)
+ model.Session.add(neoclassical_tag)
+
+ neofolk_tag = model.Tag(name='neofolk', vocabulary_id=genre_vocab.id)
+ model.Session.add(neofolk_tag)
+
+ neomedieval_tag = model.Tag(name='neomedieval',
+ vocabulary_id=genre_vocab.id)
+ model.Session.add(neomedieval_tag)
+
+ neoprog_tag = model.Tag(name='neoprog',
+ vocabulary_id=genre_vocab.id)
+ model.Session.add(neoprog_tag)
+
+ neopsychedelia_tag = model.Tag(name='neopsychedelia',
+ vocabulary_id=genre_vocab.id)
+ model.Session.add(neopsychedelia_tag)
+
+ neosoul_tag = model.Tag(name='neosoul', vocabulary_id=genre_vocab.id)
+ model.Session.add(neosoul_tag)
+
+ nerdcore_tag = model.Tag(name='nerdcore', vocabulary_id=genre_vocab.id)
+ model.Session.add(nerdcore_tag)
+
+ model.Package.get('warandpeace').add_tag(bach_tag)
+ model.Package.get('annakarenina').add_tag(sonata_tag)
+
+ model.Session.commit()
+
+
+
+search_items = [{'name':'gils',
+ 'title':'Government Information Locator Service',
+ 'url':'',
+ 'tags':'registry,country-usa,government,federal,gov,workshop-20081101,penguin'.split(','),
+ 'resources':[{'url':'http://www.dcsf.gov.uk/rsgateway/DB/SFR/s000859/SFR17_2009_tables.xls',
+ 'format':'XLS',
+ 'last_modified': datetime.datetime(2005, 10, 1),
+ 'description':'December 2009 | http://www.statistics.gov.uk/hub/id/119-36345'},
+ {'url':'http://www.dcsf.gov.uk/rsgateway/DB/SFR/s000860/SFR17_2009_key.doc',
+ 'format':'DOC',
+ 'description':'http://www.statistics.gov.uk/hub/id/119-34565'}],
+ 'groups':'ukgov test1 test2 penguin',
+ 'license':'odc-by',
+ 'notes':u'''From
+
+> The Government Information Locator Service (GILS) is an effort to identify, locate, and describe publicly available Federal
+> Because this collection is decentralized, the GPO
+
+Foreign word:
+u with umlaut th\xfcmb
+''',
+ 'extras':{'date_released':'2008'},
+ },
+ {'name':'us-gov-images',
+ 'title':'U.S. Government Photos and Graphics',
+ 'url':'http://www.usa.gov/Topics/Graphics.shtml',
+ 'download_url':'http://www.usa.gov/Topics/Graphics.shtml',
+ 'tags':'images,graphics,photographs,photos,pictures,us,usa,america,history,wildlife,nature,war,military,todo split,gov,penguin'.split(','),
+ 'groups':'ukgov test1 penguin',
+ 'license':'other-open',
+ 'notes':'''## About
+
+Collection of links to different US image collections in the public domain.
+
+## Openness
+
+> Most of these images and graphics are available for use in the public domain, and''',
+ 'extras':{'date_released':'2009'},
+ },
+ {'name':'usa-courts-gov',
+ 'title':'Text of US Federal Cases',
+ 'url':'http://bulk.resource.org/courts.gov/',
+ 'download_url':'http://bulk.resource.org/courts.gov/',
+ 'tags':'us,courts,case-law,us,courts,case-law,gov,legal,law,access-bulk,penguins,penguin'.split(','),
+ 'groups':'ukgov test2 penguin',
+ 'license':'cc-zero',
+ 'notes':'''### Description
+
+1.8 million pages of U.S. case law available with no restrictions. From the [README](http://bulk.resource.org/courts.gov/0_README.html):
+
+> This file is http://bulk.resource.org/courts.gov/0_README.html and was last revised.
+
+penguin
+''',
+ 'extras':{'date_released':'2007-06'},
+ },
+ {'name':'uk-government-expenditure',
+ 'title':'UK Government Expenditure',
+ 'tags':'workshop-20081101,uk,gov,expenditure,finance,public,funding,penguin'.split(','),
+ 'groups':'ukgov penguin',
+ 'notes':'''Discussed at [Workshop on Public Information, 2008-11-02](http://okfn.org/wiki/PublicInformation).
+
+Overview is available in Red Book, or Financial Statement and Budget Report (FSBR), [published by the Treasury](http://www.hm-treasury.gov.uk/budget.htm).''',
+ 'extras':{'date_released':'2007-10'},
+ },
+ {'name':'se-publications',
+ 'title':'Sweden - Government Offices of Sweden - Publications',
+ 'url':'http://www.sweden.gov.se/sb/d/574',
+ 'groups':'penguin',
+ 'tags':u'country-sweden,format-pdf,access-www,documents,publications,government,eutransparency,penguin,CAPITALS,surprise.,greek omega \u03a9,japanese katakana \u30a1'.split(','),
+ 'license':'',
+ 'notes':'''### About
+
+Official documents including "government bills and reports, information material and other publications".
+
+### Reuse
+
+Not clear.''',
+ 'extras':{'date_released':'2009-10-27'},
+ },
+ {'name':'se-opengov',
+ 'title':'Opengov.se',
+ 'groups':'penguin',
+ 'url':'http://www.opengov.se/',
+ 'download_url':'http://www.opengov.se/data/open/',
+ 'tags':'country-sweden,government,data,penguin'.split(','),
+ 'license':'cc-by-sa',
+ 'notes':'''### About
+
+From [website](http://www.opengov.se/sidor/english/):
+
+> Opengov.se is an initiative to highlight available public datasets in Sweden. It contains a commentable catalog of government datasets, their formats and usage restrictions.
+
+> The goal is to highlight the benefits of open access to government data and explain how this is done in practice.
+
+### Openness
+
+It appears that the website is under a CC-BY-SA license. Legal status of the data varies. Data that is fully open can be viewed at:
+
+ * '''
+ },
+ ]
+
+family_items = [{'name':u'abraham', 'title':u'Abraham'},
+ {'name':u'homer', 'title':u'Homer'},
+ {'name':u'homer_derived', 'title':u'Homer Derived'},
+ {'name':u'beer', 'title':u'Beer'},
+ {'name':u'bart', 'title':u'Bart'},
+ {'name':u'lisa', 'title':u'Lisa'},
+ {'name':u'marge', 'title':u'Marge'},
+ ]
+family_relationships = [('abraham', 'parent_of', 'homer'),
+ ('homer', 'parent_of', 'bart'),
+ ('homer', 'parent_of', 'lisa'),
+ ('marge', 'parent_of', 'lisa'),
+ ('marge', 'parent_of', 'bart'),
+ ('homer_derived', 'derives_from', 'homer'),
+ ('homer', 'depends_on', 'beer'),
+ ]
+
+gov_items = [
+ {'name':'private-fostering-england-2009',
+ 'title':'Private Fostering',
+ 'notes':'Figures on children cared for and accommodated in private fostering arrangements, England, Year ending 31 March 2009',
+ 'resources':[{'url':'http://www.dcsf.gov.uk/rsgateway/DB/SFR/s000859/SFR17_2009_tables.xls',
+ 'format':'XLS',
+ 'description':'December 2009 | http://www.statistics.gov.uk/hub/id/119-36345'},
+ {'url':'http://www.dcsf.gov.uk/rsgateway/DB/SFR/s000860/SFR17_2009_key.doc',
+ 'format':'DOC',
+ 'description':'http://www.statistics.gov.uk/hub/id/119-34565'}],
+ 'url':'http://www.dcsf.gov.uk/rsgateway/DB/SFR/s000859/index.shtml',
+ 'author':'DCSF Data Services Group',
+ 'author_email':'statistics@dcsf.gsi.gov.uk',
+ 'license':'ukcrown',
+ 'tags':'children fostering',
+ 'extras':{
+ 'external_reference':'DCSF-DCSF-0024',
+ 'date_released':'2009-07-30',
+ 'date_updated':'2009-07-30',
+ 'update_frequency':'annually',
+ 'geographic_granularity':'regional',
+ 'geographic_coverage':'100000: England',
+ 'department':'Department for Education',
+ 'published_by':'Department for Education [3]',
+ 'published_via':'',
+ 'temporal_granularity':'years',
+ 'temporal_coverage-from':'2008-6',
+ 'temporal_coverage-to':'2009-6',
+ 'mandate':'',
+ 'national_statistic':'yes',
+ 'precision':'Numbers to nearest 10, percentage to nearest whole number',
+ 'taxonomy_url':'',
+ 'agency':'',
+ 'import_source':'ONS-Jan-09',
+ }
+ },
+ {'name':'weekly-fuel-prices',
+ 'title':'Weekly fuel prices',
+ 'notes':'Latest price as at start of week of unleaded petrol and diesel.',
+ 'resources':[{'url':'http://www.decc.gov.uk/assets/decc/statistics/source/prices/qep211.xls', 'format':'XLS', 'description':'Quarterly 23/2/12'}],
+ 'url':'http://www.decc.gov.uk/en/content/cms/statistics/source/prices/prices.aspx',
+ 'author':'DECC Energy Statistics Team',
+ 'author_email':'energy.stats@decc.gsi.gov.uk',
+ 'license':'ukcrown',
+ 'tags':'fuel prices',
+ 'extras':{
+ 'external_reference':'DECC-DECC-0001',
+ 'date_released':'2009-11-24',
+ 'date_updated':'2009-11-24',
+ 'update_frequency':'weekly',
+ 'geographic_granularity':'national',
+ 'geographic_coverage':'111100: United Kingdom (England, Scotland, Wales, Northern Ireland)',
+ 'department':'Department of Energy and Climate Change',
+ 'published_by':'Department of Energy and Climate Change [4]',
+ 'published_via':'',
+ 'mandate':'',
+ 'temporal_granularity':'weeks',
+ 'temporal_coverage-from':'2008-11-24',
+ 'temporal_coverage-to':'2009-11-24',
+ 'national_statistic':'no',
+ 'import_source':'DECC-Jan-09',
+ }
+ }
+ ]
+
+group_hierarchy_groups = [
+ {'name': 'department-of-health',
+ 'title': 'Department of Health',
+ 'contact-email': 'contact@doh.gov.uk',
+ 'type': 'organization',
+ 'is_organization': True
+ },
+ {'name': 'food-standards-agency',
+ 'title': 'Food Standards Agency',
+ 'contact-email': 'contact@fsa.gov.uk',
+ 'parent': 'department-of-health',
+ 'type': 'organization',
+ 'is_organization': True},
+ {'name': 'national-health-service',
+ 'title': 'National Health Service',
+ 'contact-email': 'contact@nhs.gov.uk',
+ 'parent': 'department-of-health',
+ 'type': 'organization',
+ 'is_organization': True,
+ 'editors': ['nhseditor'],
+ 'admins': ['nhsadmin']},
+ {'name': 'nhs-wirral-ccg',
+ 'title': 'NHS Wirral CCG',
+ 'contact-email': 'contact@wirral.nhs.gov.uk',
+ 'parent': 'national-health-service',
+ 'type': 'organization',
+ 'is_organization': True,
+ 'editors': ['wirraleditor'],
+ 'admins': ['wirraladmin']},
+ {'name': 'nhs-southwark-ccg',
+ 'title': 'NHS Southwark CCG',
+ 'contact-email': 'contact@southwark.nhs.gov.uk',
+ 'parent': 'national-health-service',
+ 'type': 'organization',
+ 'is_organization': True},
+ {'name': 'cabinet-office',
+ 'title': 'Cabinet Office',
+ 'contact-email': 'contact@cabinet-office.gov.uk',
+ 'type': 'organization',
+ 'is_organization': True},
+ ]
+
+group_hierarchy_datasets = [
+ {'name': 'doh-spend', 'title': 'Department of Health Spend Data',
+ 'groups': ['department-of-health']},
+ {'name': 'nhs-spend', 'title': 'NHS Spend Data',
+ 'groups': ['national-health-service']},
+ {'name': 'wirral-spend', 'title': 'Wirral Spend Data',
+ 'groups': ['nhs-wirral-ccg']},
+ {'name': 'southwark-spend', 'title': 'Southwark Spend Data',
+ 'groups': ['nhs-southwark-ccg']},
+ ]
+
+group_hierarchy_users = [{'name': 'nhsadmin', 'password': 'pass'},
+ {'name': 'nhseditor', 'password': 'pass'},
+ {'name': 'wirraladmin', 'password': 'pass'},
+ {'name': 'wirraleditor', 'password': 'pass'},
+ ]
+
+# Some test terms and translations.
+terms = ('A Novel By Tolstoy',
+ 'Index of the novel',
+ 'russian',
+ 'tolstoy',
+ "Dave's books",
+ "Roger's books",
+ 'romantic novel',
+ 'book',
+ '123',
+ '456',
+ '789',
+ 'plain text',
+ 'Roger likes these books.',
+)
+english_translations = {
+ '123': 'jealousy',
+ '456': 'realism',
+ '789': 'hypocrisy',
+}
+german_translations = {
+ 'A Novel By Tolstoy': 'Roman von Tolstoi',
+ 'Index of the novel': 'Index des Romans',
+ 'russian': 'Russisch',
+ 'tolstoy': 'Tolstoi',
+ "Dave's books": 'Daves Bucher',
+ "Roger's books": 'Rogers Bucher',
+ 'romantic novel': 'Liebesroman',
+ 'book': 'Buch',
+ '456': 'Realismus',
+ '789': 'Heuchelei',
+ 'plain text': 'Klartext',
+ 'Roger likes these books.': 'Roger mag diese Bucher.'
+}
+french_translations = {
+ 'A Novel By Tolstoy': 'A Novel par Tolstoi',
+ 'Index of the novel': 'Indice du roman',
+ 'russian': 'russe',
+ 'romantic novel': 'roman romantique',
+ 'book': 'livre',
+ '123': 'jalousie',
+ '789': 'hypocrisie',
+}
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/datapreview.py b/venv/lib/python2.7/site-packages/ckan/lib/datapreview.py
new file mode 100644
index 00000000..cb995de2
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/datapreview.py
@@ -0,0 +1,306 @@
+# encoding: utf-8
+
+"""Data previewer functions
+
+Functions and data structures that are needed for the ckan data preview.
+"""
+
+import urlparse
+import logging
+
+from ckan.common import config
+
+import ckan.plugins as p
+from ckan import logic
+from ckan.common import _
+
+
+log = logging.getLogger(__name__)
+
+
+DEFAULT_RESOURCE_VIEW_TYPES = ['image_view', 'recline_view']
+
+
+def res_format(resource):
+ ''' The assumed resource format in lower case. '''
+ if not resource['url']:
+ return None
+ return (resource['format'] or resource['url'].split('.')[-1]).lower()
+
+
+def compare_domains(urls):
+ ''' Return True if the domains of the provided urls are the same.
+ '''
+ first_domain = None
+ for url in urls:
+ # all urls are interpreted as absolute urls,
+ # except for urls that start with a /
+ try:
+ if not urlparse.urlparse(url).scheme and not url.startswith('/'):
+ url = '//' + url
+ parsed = urlparse.urlparse(url.lower(), 'http')
+ domain = (parsed.scheme, parsed.hostname, parsed.port)
+ except ValueError:
+ # URL is so messed up that even urlparse can't stand it
+ return False
+
+ if not first_domain:
+ first_domain = domain
+ continue
+ if first_domain != domain:
+ return False
+ return True
+
+
+def on_same_domain(data_dict):
+ # compare CKAN domain and resource URL
+ ckan_url = config.get('ckan.site_url', '//localhost:5000')
+ resource_url = data_dict['resource']['url']
+
+ return compare_domains([ckan_url, resource_url])
+
+
+def get_preview_plugin(data_dict, return_first=False):
+ '''Determines whether there is an extension that can preview the resource.
+
+ :param data_dict: contains a resource and package dict.
+ The resource dict has to have a value for ``on_same_domain``
+ :type data_dict: dictionary
+
+ :param return_first: If True return the first plugin that can preview
+ :type return_first: bool
+
+ Returns a dict of plugins that can preview or ones that are fixable'''
+
+ data_dict['resource']['on_same_domain'] = on_same_domain(data_dict)
+
+ plugins_that_can_preview = []
+ plugins_fixable = []
+ for plugin in p.PluginImplementations(p.IResourcePreview):
+ p_info = {'plugin': plugin, 'quality': 1}
+ data = plugin.can_preview(data_dict)
+ # old school plugins return true/False
+ if isinstance(data, bool):
+ p_info['can_preview'] = data
+ else:
+ # new school provide a dict
+ p_info.update(data)
+ # if we can preview
+ if p_info['can_preview']:
+ if return_first:
+ plugin
+ plugins_that_can_preview.append(p_info)
+ elif p_info.get('fixable'):
+ plugins_fixable.append(p_info)
+
+ num_plugins = len(plugins_that_can_preview)
+ if num_plugins == 0:
+ # we didn't find any. see if any could be made to work
+ for plug in plugins_fixable:
+ log.info('%s would allow previews. To fix: %s' % (
+ plug['plugin'], plug['fixable']))
+ preview_plugin = None
+ elif num_plugins == 1:
+ # just one available
+ preview_plugin = plugins_that_can_preview[0]['plugin']
+ else:
+ # multiple plugins so get the best one
+ plugs = [pl['plugin'] for pl in plugins_that_can_preview]
+ log.warn('Multiple previews are possible. {0}'.format(plugs))
+ preview_plugin = max(plugins_that_can_preview,
+ key=lambda x: x['quality'])['plugin']
+ return preview_plugin
+
+
+def get_view_plugin(view_type):
+ '''
+ Returns the IResourceView plugin associated with the given view_type.
+ '''
+ for plugin in p.PluginImplementations(p.IResourceView):
+ info = plugin.info()
+ name = info.get('name')
+ if name == view_type:
+ return plugin
+
+
+def get_view_plugins(view_types):
+ '''
+ Returns a list of the view plugins associated with the given view_types.
+ '''
+ view_plugins = []
+ for view_type in view_types:
+ view_plugin = get_view_plugin(view_type)
+
+ if view_plugin:
+ view_plugins.append(view_plugin)
+ return view_plugins
+
+
+def get_allowed_view_plugins(data_dict):
+ '''
+ Returns a list of view plugins that work against the resource provided
+
+ The ``data_dict`` contains: ``resource`` and ``package``.
+ '''
+ can_view = []
+ for plugin in p.PluginImplementations(p.IResourceView):
+
+ plugin_info = plugin.info()
+
+ if (plugin_info.get('always_available', False) or
+ plugin.can_view(data_dict)):
+ can_view.append(plugin)
+ return can_view
+
+
+def get_default_view_plugins(get_datastore_views=False):
+ '''
+ Returns the list of view plugins to be created by default on new resources
+
+ The default view types are defined via the `ckan.views.default_views`
+ configuration option. If this is not set (as opposed to empty, which means
+ no default views), the value of DEFAULT_RESOURCE_VIEW_TYPES is used to
+ look up the plugins.
+
+ If get_datastore_views is False, only the ones not requiring data to be in
+ the DataStore are returned, and if True, only the ones requiring it are.
+
+ To flag a view plugin as requiring the DataStore, it must have the
+ `requires_datastore` key set to True in the dict returned by its `info()`
+ method.
+
+ Returns a list of IResourceView plugins
+ '''
+
+ if config.get('ckan.views.default_views') is None:
+ default_view_types = DEFAULT_RESOURCE_VIEW_TYPES
+ else:
+ default_view_types = config.get('ckan.views.default_views').split()
+
+ default_view_plugins = []
+ for view_type in default_view_types:
+
+ view_plugin = get_view_plugin(view_type)
+
+ if not view_plugin:
+ log.warn('Plugin for view {0} could not be found'
+ .format(view_type))
+ # We should probably check on startup if the default
+ # view types exist
+ continue
+
+ info = view_plugin.info()
+
+ plugin_requires_datastore = info.get('requires_datastore', False)
+
+ if plugin_requires_datastore == get_datastore_views:
+ default_view_plugins.append(view_plugin)
+
+ return default_view_plugins
+
+
+def add_views_to_resource(context,
+ resource_dict,
+ dataset_dict=None,
+ view_types=[],
+ create_datastore_views=False):
+ '''
+ Creates the provided views (if necessary) on the provided resource
+
+ Views to create are provided as a list of ``view_types``. If no types are
+ provided, the default views defined in the ``ckan.views.default_views``
+ will be created.
+
+ The function will get the plugins for the default views defined in
+ the configuration, and if some were found the `can_view` method of
+ each one of them will be called to determine if a resource view should
+ be created.
+
+ Resource views extensions get the resource dict and the parent dataset
+ dict. If the latter is not provided, `package_show` is called to get it.
+
+ By default only view plugins that don't require the resource data to be in
+ the DataStore are called. This is only relevant when the default view
+ plugins are used, not when explicitly passing view types. See
+ :py:func:`ckan.logic.action.create.package_create_default_resource_views.``
+ for details on the ``create_datastore_views`` parameter.
+
+ Returns a list of resource views created (empty if none were created)
+ '''
+ if not dataset_dict:
+ dataset_dict = logic.get_action('package_show')(
+ context, {'id': resource_dict['package_id']})
+
+ if not view_types:
+ view_plugins = get_default_view_plugins(create_datastore_views)
+ else:
+ view_plugins = get_view_plugins(view_types)
+
+ if not view_plugins:
+ return []
+
+ existing_views = p.toolkit.get_action('resource_view_list')(
+ context, {'id': resource_dict['id']})
+
+ existing_view_types = ([v['view_type'] for v in existing_views]
+ if existing_views
+ else [])
+
+ created_views = []
+ for view_plugin in view_plugins:
+
+ view_info = view_plugin.info()
+
+ # Check if a view of this type already exists
+ if view_info['name'] in existing_view_types:
+ continue
+
+ # Check if a view of this type can preview this resource
+ if view_plugin.can_view({
+ 'resource': resource_dict,
+ 'package': dataset_dict
+ }):
+ view = {'resource_id': resource_dict['id'],
+ 'view_type': view_info['name'],
+ 'title': view_info.get('default_title', _('View')),
+ 'description': view_info.get('default_description', '')}
+
+ view_dict = p.toolkit.get_action('resource_view_create')(context,
+ view)
+ created_views.append(view_dict)
+
+ return created_views
+
+
+def add_views_to_dataset_resources(context,
+ dataset_dict,
+ view_types=[],
+ create_datastore_views=False):
+ '''
+ Creates the provided views on all resources of the provided dataset
+
+ Views to create are provided as a list of ``view_types``. If no types are
+ provided, the default views defined in the ``ckan.views.default_views``
+ will be created. Note that in both cases only these views that can render
+ the resource will be created (ie its view plugin ``can_view`` method
+ returns True.
+
+ By default only view plugins that don't require the resource data to be in
+ the DataStore are called. This is only relevant when the default view
+ plugins are used, not when explicitly passing view types. See
+ :py:func:`ckan.logic.action.create.package_create_default_resource_views.``
+ for details on the ``create_datastore_views`` parameter.
+
+ Returns a list of resource views created (empty if none were created)
+ '''
+
+ created_views = []
+ for resource_dict in dataset_dict.get('resources', []):
+ new_views = add_views_to_resource(context,
+ resource_dict,
+ dataset_dict,
+ view_types,
+ create_datastore_views)
+ created_views.extend(new_views)
+
+ return created_views
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/dictization/__init__.py b/venv/lib/python2.7/site-packages/ckan/lib/dictization/__init__.py
new file mode 100644
index 00000000..3d8e1576
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/dictization/__init__.py
@@ -0,0 +1,156 @@
+# encoding: utf-8
+
+import datetime
+from sqlalchemy.orm import class_mapper
+import sqlalchemy
+from six import text_type
+from ckan.common import config
+from ckan.model.core import State
+
+try:
+ RowProxy = sqlalchemy.engine.result.RowProxy
+except AttributeError:
+ RowProxy = sqlalchemy.engine.base.RowProxy
+
+try:
+ long # Python 2
+except NameError:
+ long = int # Python 3
+
+
+# NOTE
+# The functions in this file contain very generic methods for dictizing objects
+# and saving dictized objects. If a specialised use is needed please do NOT extend
+# these functions. Copy code from here as needed.
+
+
+def table_dictize(obj, context, **kw):
+ '''Get any model object and represent it as a dict'''
+
+ result_dict = {}
+
+ model = context["model"]
+ session = model.Session
+
+ if isinstance(obj, RowProxy):
+ fields = obj.keys()
+ else:
+ ModelClass = obj.__class__
+ table = class_mapper(ModelClass).mapped_table
+ fields = [field.name for field in table.c]
+
+ for field in fields:
+ name = field
+ if name in ('current', 'expired_timestamp', 'expired_id'):
+ continue
+ if name == 'continuity_id':
+ continue
+ value = getattr(obj, name)
+ if value is None:
+ result_dict[name] = value
+ elif isinstance(value, dict):
+ result_dict[name] = value
+ elif isinstance(value, int):
+ result_dict[name] = value
+ elif isinstance(value, long):
+ result_dict[name] = value
+ elif isinstance(value, datetime.datetime):
+ result_dict[name] = value.isoformat()
+ elif isinstance(value, list):
+ result_dict[name] = value
+ else:
+ result_dict[name] = text_type(value)
+
+ result_dict.update(kw)
+
+ ##HACK For optimisation to get metadata_modified created faster.
+
+ context['metadata_modified'] = max(result_dict.get('revision_timestamp', ''),
+ context.get('metadata_modified', ''))
+
+ return result_dict
+
+
+def obj_list_dictize(obj_list, context, sort_key=lambda x:x):
+ '''Get a list of model object and represent it as a list of dicts'''
+
+ result_list = []
+ active = context.get('active', True)
+
+ for obj in obj_list:
+ if context.get('with_capacity'):
+ obj, capacity = obj
+ dictized = table_dictize(obj, context, capacity=capacity)
+ else:
+ dictized = table_dictize(obj, context)
+ if active and obj.state != 'active':
+ continue
+ result_list.append(dictized)
+
+ return sorted(result_list, key=sort_key)
+
+def obj_dict_dictize(obj_dict, context, sort_key=lambda x:x):
+ '''Get a dict whose values are model objects
+ and represent it as a list of dicts'''
+
+ result_list = []
+
+ for key, obj in obj_dict.items():
+ result_list.append(table_dictize(obj, context))
+
+ return sorted(result_list, key=sort_key)
+
+
+def get_unique_constraints(table, context):
+ '''Get a list of unique constraints for a sqlalchemy table'''
+
+ list_of_constraints = []
+
+ for contraint in table.constraints:
+ if isinstance(contraint, sqlalchemy.UniqueConstraint):
+ columns = [column.name for column in contraint.columns]
+ list_of_constraints.append(columns)
+
+ return list_of_constraints
+
+def table_dict_save(table_dict, ModelClass, context):
+ '''Given a dict and a model class, update or create a sqlalchemy object.
+ This will use an existing object if "id" is supplied OR if any unique
+ constraints are met. e.g supplying just a tag name will get out that tag obj.
+ '''
+
+ model = context["model"]
+ session = context["session"]
+
+ table = class_mapper(ModelClass).mapped_table
+
+ obj = None
+
+ id = table_dict.get("id")
+
+ if id:
+ obj = session.query(ModelClass).get(id)
+
+ if not obj:
+ unique_constraints = get_unique_constraints(table, context)
+ for constraint in unique_constraints:
+ params = dict((key, table_dict.get(key)) for key in constraint)
+ obj = session.query(ModelClass).filter_by(**params).first()
+ if obj:
+ if 'name' in params and getattr(obj, 'state', None) == State.DELETED:
+ obj.name = obj.id
+ obj = None
+ else:
+ break
+
+ if not obj:
+ obj = ModelClass()
+
+ for key, value in table_dict.iteritems():
+ if isinstance(value, list):
+ continue
+ setattr(obj, key, value)
+
+ session.add(obj)
+
+ return obj
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/dictization/model_dictize.py b/venv/lib/python2.7/site-packages/ckan/lib/dictization/model_dictize.py
new file mode 100644
index 00000000..9acb1509
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/dictization/model_dictize.py
@@ -0,0 +1,768 @@
+# encoding: utf-8
+
+'''
+These dictize functions generally take a domain object (such as Package) and
+convert it to a dictionary, including related objects (e.g. for Package it
+includes PackageTags, PackageExtras, PackageGroup etc).
+
+The basic recipe is to call:
+
+ dictized = ckan.lib.dictization.table_dictize(domain_object)
+
+which builds the dictionary by iterating over the table columns.
+'''
+import datetime
+import urlparse
+
+from ckan.common import config
+from sqlalchemy.sql import select
+
+import ckan.logic as logic
+import ckan.plugins as plugins
+import ckan.lib.helpers as h
+import ckan.lib.dictization as d
+import ckan.authz as authz
+import ckan.lib.search as search
+import ckan.lib.munge as munge
+
+## package save
+
+def group_list_dictize(obj_list, context,
+ sort_key=lambda x: x['display_name'], reverse=False,
+ with_package_counts=True,
+ include_groups=False,
+ include_tags=False,
+ include_extras=False):
+
+ group_dictize_context = dict(context.items()[:])
+ # Set options to avoid any SOLR queries for each group, which would
+ # slow things further.
+ group_dictize_options = {
+ 'packages_field': 'dataset_count' if with_package_counts else None,
+ # don't allow packages_field='datasets' as it is too slow
+ 'include_groups': include_groups,
+ 'include_tags': include_tags,
+ 'include_extras': include_extras,
+ 'include_users': False, # too slow - don't allow
+ }
+ if with_package_counts and 'dataset_counts' not in group_dictize_context:
+ # 'dataset_counts' will already be in the context in the case that
+ # group_list_dictize recurses via group_dictize (groups in groups)
+ group_dictize_context['dataset_counts'] = get_group_dataset_counts()
+ if context.get('with_capacity'):
+ group_list = [group_dictize(group, group_dictize_context,
+ capacity=capacity, **group_dictize_options)
+ for group, capacity in obj_list]
+ else:
+ group_list = [group_dictize(group, group_dictize_context,
+ **group_dictize_options)
+ for group in obj_list]
+
+ return sorted(group_list, key=sort_key, reverse=reverse)
+
+def resource_list_dictize(res_list, context):
+
+ active = context.get('active', True)
+ result_list = []
+ for res in res_list:
+ resource_dict = resource_dictize(res, context)
+ if active and res.state != 'active':
+ continue
+
+ result_list.append(resource_dict)
+
+ return sorted(result_list, key=lambda x: x["position"])
+
+def extras_dict_dictize(extras_dict, context):
+ result_list = []
+ for name, extra in extras_dict.iteritems():
+ dictized = d.table_dictize(extra, context)
+ if not extra.state == 'active':
+ continue
+ value = dictized["value"]
+ result_list.append(dictized)
+
+ return sorted(result_list, key=lambda x: x["key"])
+
+def extras_list_dictize(extras_list, context):
+ result_list = []
+ active = context.get('active', True)
+ for extra in extras_list:
+ dictized = d.table_dictize(extra, context)
+ if active and extra.state != 'active':
+ continue
+ value = dictized["value"]
+ result_list.append(dictized)
+
+ return sorted(result_list, key=lambda x: x["key"])
+
+
+def resource_dictize(res, context):
+ model = context['model']
+ resource = d.table_dictize(res, context)
+ extras = resource.pop("extras", None)
+ if extras:
+ resource.update(extras)
+ # some urls do not have the protocol this adds http:// to these
+ url = resource['url']
+ ## for_edit is only called at the times when the dataset is to be edited
+ ## in the frontend. Without for_edit the whole qualified url is returned.
+ if resource.get('url_type') == 'upload' and not context.get('for_edit'):
+ url = url.rsplit('/')[-1]
+ cleaned_name = munge.munge_filename(url)
+ resource['url'] = h.url_for(controller='package',
+ action='resource_download',
+ id=resource['package_id'],
+ resource_id=res.id,
+ filename=cleaned_name,
+ qualified=True)
+ elif resource['url'] and not urlparse.urlsplit(url).scheme and not context.get('for_edit'):
+ resource['url'] = u'http://' + url.lstrip('/')
+ return resource
+
+
+def _execute(q, table, context):
+ '''
+ Takes an SqlAlchemy query (q) that is (at its base) a Select on an
+ object table (table), and it returns the object.
+
+ Analogous with _execute_with_revision, so takes the same params, even
+ though it doesn't need the table.
+ '''
+ model = context['model']
+ session = model.Session
+ return session.execute(q)
+
+
+def _execute_with_revision(q, rev_table, context):
+ '''
+ Takes an SqlAlchemy query (q) that is (at its base) a Select on an object
+ revision table (rev_table), and you provide revision_id or revision_date in
+ the context and it will filter the object revision(s) to an earlier time.
+
+ Raises NotFound if context['revision_id'] is provided, but the revision
+ ID does not exist.
+
+ Returns [] if there are no results.
+
+ '''
+ model = context['model']
+ session = model.Session
+ revision_id = context.get('revision_id')
+ revision_date = context.get('revision_date')
+
+ if revision_id:
+ revision = session.query(context['model'].Revision).filter_by(
+ id=revision_id).first()
+ if not revision:
+ raise logic.NotFound
+ revision_date = revision.timestamp
+
+ q = q.where(rev_table.c.revision_timestamp <= revision_date)
+ q = q.where(rev_table.c.expired_timestamp > revision_date)
+
+ return session.execute(q)
+
+
+def package_dictize(pkg, context):
+ '''
+ Given a Package object, returns an equivalent dictionary.
+
+ Normally this is the most recent version, but you can provide revision_id
+ or revision_date in the context and it will filter to an earlier time.
+
+ May raise NotFound if:
+ * the specified revision_id doesn't exist
+ * the specified revision_date was before the package was created
+ '''
+ model = context['model']
+ is_latest_revision = not(context.get('revision_id') or
+ context.get('revision_date'))
+ execute = _execute if is_latest_revision else _execute_with_revision
+ #package
+ if is_latest_revision:
+ if isinstance(pkg, model.PackageRevision):
+ pkg = model.Package.get(pkg.id)
+ result = pkg
+ else:
+ package_rev = model.package_revision_table
+ q = select([package_rev]).where(package_rev.c.id == pkg.id)
+ result = execute(q, package_rev, context).first()
+ if not result:
+ raise logic.NotFound
+ result_dict = d.table_dictize(result, context)
+ #strip whitespace from title
+ if result_dict.get('title'):
+ result_dict['title'] = result_dict['title'].strip()
+
+ #resources
+ if is_latest_revision:
+ res = model.resource_table
+ else:
+ res = model.resource_revision_table
+ q = select([res]).where(res.c.package_id == pkg.id)
+ result = execute(q, res, context)
+ result_dict["resources"] = resource_list_dictize(result, context)
+ result_dict['num_resources'] = len(result_dict.get('resources', []))
+
+ #tags
+ tag = model.tag_table
+ if is_latest_revision:
+ pkg_tag = model.package_tag_table
+ else:
+ pkg_tag = model.package_tag_revision_table
+ q = select([tag, pkg_tag.c.state],
+ from_obj=pkg_tag.join(tag, tag.c.id == pkg_tag.c.tag_id)
+ ).where(pkg_tag.c.package_id == pkg.id)
+ result = execute(q, pkg_tag, context)
+ result_dict["tags"] = d.obj_list_dictize(result, context,
+ lambda x: x["name"])
+ result_dict['num_tags'] = len(result_dict.get('tags', []))
+
+ # Add display_names to tags. At first a tag's display_name is just the
+ # same as its name, but the display_name might get changed later (e.g.
+ # translated into another language by the multilingual extension).
+ for tag in result_dict['tags']:
+ assert not 'display_name' in tag
+ tag['display_name'] = tag['name']
+
+ #extras
+ if is_latest_revision:
+ extra = model.package_extra_table
+ else:
+ extra = model.extra_revision_table
+ q = select([extra]).where(extra.c.package_id == pkg.id)
+ result = execute(q, extra, context)
+ result_dict["extras"] = extras_list_dictize(result, context)
+
+ #groups
+ if is_latest_revision:
+ member = model.member_table
+ else:
+ member = model.member_revision_table
+ group = model.group_table
+ q = select([group, member.c.capacity],
+ from_obj=member.join(group, group.c.id == member.c.group_id)
+ ).where(member.c.table_id == pkg.id)\
+ .where(member.c.state == 'active') \
+ .where(group.c.is_organization == False)
+ result = execute(q, member, context)
+ context['with_capacity'] = False
+ ## no package counts as cannot fetch from search index at the same
+ ## time as indexing to it.
+ ## tags, extras and sub-groups are not included for speed
+ result_dict["groups"] = group_list_dictize(result, context,
+ with_package_counts=False)
+
+ #owning organization
+ if is_latest_revision:
+ group = model.group_table
+ else:
+ group = model.group_revision_table
+ q = select([group]
+ ).where(group.c.id == pkg.owner_org) \
+ .where(group.c.state == 'active')
+ result = execute(q, group, context)
+ organizations = d.obj_list_dictize(result, context)
+ if organizations:
+ result_dict["organization"] = organizations[0]
+ else:
+ result_dict["organization"] = None
+
+ #relations
+ if is_latest_revision:
+ rel = model.package_relationship_table
+ else:
+ rel = model.package_relationship_revision_table
+ q = select([rel]).where(rel.c.subject_package_id == pkg.id)
+ result = execute(q, rel, context)
+ result_dict["relationships_as_subject"] = \
+ d.obj_list_dictize(result, context)
+ q = select([rel]).where(rel.c.object_package_id == pkg.id)
+ result = execute(q, rel, context)
+ result_dict["relationships_as_object"] = \
+ d.obj_list_dictize(result, context)
+
+ # Extra properties from the domain object
+ # We need an actual Package object for this, not a PackageRevision
+ if isinstance(pkg, model.PackageRevision):
+ pkg = model.Package.get(pkg.id)
+
+ # isopen
+ result_dict['isopen'] = pkg.isopen if isinstance(pkg.isopen, bool) \
+ else pkg.isopen()
+
+ # type
+ # if null assign the default value to make searching easier
+ result_dict['type'] = pkg.type or u'dataset'
+
+ # license
+ if pkg.license and pkg.license.url:
+ result_dict['license_url'] = pkg.license.url
+ result_dict['license_title'] = pkg.license.title.split('::')[-1]
+ elif pkg.license:
+ result_dict['license_title'] = pkg.license.title
+ else:
+ result_dict['license_title'] = pkg.license_id
+
+ # creation and modification date
+ result_dict['metadata_modified'] = pkg.metadata_modified.isoformat()
+ result_dict['metadata_created'] = pkg.metadata_created.isoformat() \
+ if pkg.metadata_created else None
+
+ return result_dict
+
+
+def _get_members(context, group, member_type):
+
+ model = context['model']
+ Entity = getattr(model, member_type[:-1].capitalize())
+ q = model.Session.query(Entity, model.Member.capacity).\
+ join(model.Member, model.Member.table_id == Entity.id).\
+ filter(model.Member.group_id == group.id).\
+ filter(model.Member.state == 'active').\
+ filter(model.Member.table_name == member_type[:-1])
+ if member_type == 'packages':
+ q = q.filter(Entity.private==False)
+ if 'limits' in context and member_type in context['limits']:
+ return q[:context['limits'][member_type]]
+ return q.all()
+
+
+def get_group_dataset_counts():
+ '''For all public groups, return their dataset counts, as a SOLR facet'''
+ query = search.PackageSearchQuery()
+ q = {'q': '+capacity:public',
+ 'fl': 'groups', 'facet.field': ['groups', 'owner_org'],
+ 'facet.limit': -1, 'rows': 1}
+ query.run(q)
+ return query.facets
+
+
+def group_dictize(group, context,
+ include_groups=True,
+ include_tags=True,
+ include_users=True,
+ include_extras=True,
+ packages_field='datasets',
+ **kw):
+ '''
+ Turns a Group object and related into a dictionary. The related objects
+ like tags are included unless you specify it in the params.
+
+ :param packages_field: determines the format of the `packages` field - can
+ be `datasets`, `dataset_count` or None.
+ '''
+ assert packages_field in ('datasets', 'dataset_count', None)
+ if packages_field == 'dataset_count':
+ dataset_counts = context.get('dataset_counts', None)
+
+ result_dict = d.table_dictize(group, context)
+ result_dict.update(kw)
+
+ result_dict['display_name'] = group.title or group.name
+
+ if include_extras:
+ result_dict['extras'] = extras_dict_dictize(
+ group._extras, context)
+
+ context['with_capacity'] = True
+
+ if packages_field:
+ def get_packages_for_this_group(group_, just_the_count=False):
+ # Ask SOLR for the list of packages for this org/group
+ q = {
+ 'facet': 'false',
+ 'rows': 0,
+ }
+
+ if group_.is_organization:
+ q['fq'] = 'owner_org:"{0}"'.format(group_.id)
+ else:
+ q['fq'] = 'groups:"{0}"'.format(group_.name)
+
+ # Allow members of organizations to see private datasets.
+ if group_.is_organization:
+ is_group_member = (context.get('user') and
+ authz.has_user_permission_for_group_or_org(
+ group_.id, context.get('user'), 'read'))
+ if is_group_member:
+ q['include_private'] = True
+
+ if not just_the_count:
+ # Is there a packages limit in the context?
+ try:
+ packages_limit = context['limits']['packages']
+ except KeyError:
+ q['rows'] = 1000 # Only the first 1000 datasets are returned
+ else:
+ q['rows'] = packages_limit
+
+ search_context = dict((k, v) for (k, v) in context.items()
+ if k != 'schema')
+ search_results = logic.get_action('package_search')(search_context,
+ q)
+ return search_results['count'], search_results['results']
+
+ if packages_field == 'datasets':
+ package_count, packages = get_packages_for_this_group(group)
+ result_dict['packages'] = packages
+ else:
+ if dataset_counts is None:
+ package_count, packages = get_packages_for_this_group(
+ group, just_the_count=True)
+ else:
+ # Use the pre-calculated package_counts passed in.
+ facets = dataset_counts
+ if group.is_organization:
+ package_count = facets['owner_org'].get(group.id, 0)
+ else:
+ package_count = facets['groups'].get(group.name, 0)
+
+ result_dict['package_count'] = package_count
+
+ if include_tags:
+ # group tags are not creatable via the API yet, but that was(/is) a
+ # future intention (see kindly's commit 5c8df894 on 2011/12/23)
+ result_dict['tags'] = tag_list_dictize(
+ _get_members(context, group, 'tags'),
+ context)
+
+ if include_groups:
+ # these sub-groups won't have tags or extras for speed
+ result_dict['groups'] = group_list_dictize(
+ _get_members(context, group, 'groups'),
+ context, include_groups=True)
+
+ if include_users:
+ result_dict['users'] = user_list_dictize(
+ _get_members(context, group, 'users'),
+ context)
+
+ context['with_capacity'] = False
+
+ if context.get('for_view'):
+ if result_dict['is_organization']:
+ plugin = plugins.IOrganizationController
+ else:
+ plugin = plugins.IGroupController
+ for item in plugins.PluginImplementations(plugin):
+ result_dict = item.before_view(result_dict)
+
+ image_url = result_dict.get('image_url')
+ result_dict['image_display_url'] = image_url
+ if image_url and not image_url.startswith('http'):
+ #munge here should not have an effect only doing it incase
+ #of potential vulnerability of dodgy api input
+ image_url = munge.munge_filename_legacy(image_url)
+ result_dict['image_display_url'] = h.url_for_static(
+ 'uploads/group/%s' % result_dict.get('image_url'),
+ qualified=True
+ )
+ return result_dict
+
+def tag_list_dictize(tag_list, context):
+
+ result_list = []
+ for tag in tag_list:
+ if context.get('with_capacity'):
+ tag, capacity = tag
+ dictized = d.table_dictize(tag, context, capacity=capacity)
+ else:
+ dictized = d.table_dictize(tag, context)
+
+ # Add display_names to tag dicts. At first a tag's display_name is just
+ # the same as its name, but the display_name might get changed later
+ # (e.g. translated into another language by the multilingual
+ # extension).
+ assert not dictized.has_key('display_name')
+ dictized['display_name'] = dictized['name']
+
+ if context.get('for_view'):
+ for item in plugins.PluginImplementations(
+ plugins.ITagController):
+ dictized = item.before_view(dictized)
+
+ result_list.append(dictized)
+
+ return result_list
+
+def tag_dictize(tag, context, include_datasets=True):
+ tag_dict = d.table_dictize(tag, context)
+
+ if include_datasets:
+ query = search.PackageSearchQuery()
+
+ tag_query = u'+capacity:public '
+ vocab_id = tag_dict.get('vocabulary_id')
+
+ if vocab_id:
+ model = context['model']
+ vocab = model.Vocabulary.get(vocab_id)
+ tag_query += u'+vocab_{0}:"{1}"'.format(vocab.name, tag.name)
+ else:
+ tag_query += u'+tags:"{0}"'.format(tag.name)
+
+ q = {'q': tag_query, 'fl': 'data_dict', 'wt': 'json', 'rows': 1000}
+
+ package_dicts = [h.json.loads(result['data_dict'])
+ for result in query.run(q)['results']]
+
+ # Add display_names to tags. At first a tag's display_name is just the
+ # same as its name, but the display_name might get changed later (e.g.
+ # translated into another language by the multilingual extension).
+ assert 'display_name' not in tag_dict
+ tag_dict['display_name'] = tag_dict['name']
+
+ if context.get('for_view'):
+ for item in plugins.PluginImplementations(plugins.ITagController):
+ tag_dict = item.before_view(tag_dict)
+
+ if include_datasets:
+ tag_dict['packages'] = []
+ for package_dict in package_dicts:
+ for item in plugins.PluginImplementations(plugins.IPackageController):
+ package_dict = item.before_view(package_dict)
+ tag_dict['packages'].append(package_dict)
+ else:
+ if include_datasets:
+ tag_dict['packages'] = package_dicts
+
+ return tag_dict
+
+def user_list_dictize(obj_list, context,
+ sort_key=lambda x:x['name'], reverse=False):
+
+ result_list = []
+
+ for obj in obj_list:
+ user_dict = user_dictize(obj, context)
+ user_dict.pop('reset_key', None)
+ user_dict.pop('apikey', None)
+ user_dict.pop('email', None)
+ result_list.append(user_dict)
+ return sorted(result_list, key=sort_key, reverse=reverse)
+
+def member_dictize(member, context):
+ return d.table_dictize(member, context)
+
+def user_dictize(user, context, include_password_hash=False):
+
+ if context.get('with_capacity'):
+ user, capacity = user
+ result_dict = d.table_dictize(user, context, capacity=capacity)
+ else:
+ result_dict = d.table_dictize(user, context)
+
+ password_hash = result_dict.pop('password')
+ del result_dict['reset_key']
+
+ result_dict['display_name'] = user.display_name
+ result_dict['email_hash'] = user.email_hash
+ result_dict['number_of_edits'] = user.number_of_edits()
+ result_dict['number_created_packages'] = user.number_created_packages(
+ include_private_and_draft=context.get(
+ 'count_private_and_draft_datasets', False))
+
+ requester = context.get('user')
+
+ reset_key = result_dict.pop('reset_key', None)
+ apikey = result_dict.pop('apikey', None)
+ email = result_dict.pop('email', None)
+
+ if context.get('keep_email', False):
+ result_dict['email'] = email
+
+ if context.get('keep_apikey', False):
+ result_dict['apikey'] = apikey
+
+ if requester == user.name:
+ result_dict['apikey'] = apikey
+ result_dict['email'] = email
+
+ if authz.is_sysadmin(requester):
+ result_dict['apikey'] = apikey
+ result_dict['email'] = email
+
+ if include_password_hash:
+ result_dict['password_hash'] = password_hash
+
+ model = context['model']
+ session = model.Session
+
+ return result_dict
+
+def task_status_dictize(task_status, context):
+ return d.table_dictize(task_status, context)
+
+## conversion to api
+
+def group_to_api(group, context):
+ api_version = context.get('api_version')
+ assert api_version, 'No api_version supplied in context'
+ dictized = group_dictize(group, context)
+ dictized["extras"] = dict((extra["key"], extra["value"])
+ for extra in dictized["extras"])
+ if api_version == 1:
+ dictized["packages"] = sorted([pkg["name"] for pkg in dictized["packages"]])
+ else:
+ dictized["packages"] = sorted([pkg["id"] for pkg in dictized["packages"]])
+ return dictized
+
+def tag_to_api(tag, context):
+ api_version = context.get('api_version')
+ assert api_version, 'No api_version supplied in context'
+ dictized = tag_dictize(tag, context)
+ if api_version == 1:
+ return sorted([package["name"] for package in dictized["packages"]])
+ else:
+ return sorted([package["id"] for package in dictized["packages"]])
+
+
+def resource_dict_to_api(res_dict, package_id, context):
+ res_dict.pop("revision_id")
+ res_dict.pop("state")
+ res_dict["package_id"] = package_id
+
+
+def package_to_api(pkg, context):
+ api_version = context.get('api_version')
+ assert api_version, 'No api_version supplied in context'
+ dictized = package_dictize(pkg, context)
+
+ dictized["tags"] = [tag["name"] for tag in dictized["tags"] \
+ if not tag.get('vocabulary_id')]
+ dictized["extras"] = dict((extra["key"], extra["value"])
+ for extra in dictized["extras"])
+ dictized['license'] = pkg.license.title if pkg.license else None
+ dictized['ratings_average'] = pkg.get_average_rating()
+ dictized['ratings_count'] = len(pkg.ratings)
+ dictized['notes_rendered'] = h.render_markdown(pkg.notes)
+
+ site_url = config.get('ckan.site_url', None)
+ if site_url:
+ dictized['ckan_url'] = '%s/dataset/%s' % (site_url, pkg.name)
+
+ for resource in dictized["resources"]:
+ resource_dict_to_api(resource, pkg.id, context)
+
+ def make_api_1(package_id):
+ return pkg.get(package_id).name
+
+ def make_api_2(package_id):
+ return package_id
+
+ if api_version == 1:
+ api_fn = make_api_1
+ dictized["groups"] = [group["name"] for group in dictized["groups"]]
+ # FIXME why is this just for version 1?
+ if pkg.resources:
+ dictized['download_url'] = pkg.resources[0].url
+ else:
+ api_fn = make_api_2
+ dictized["groups"] = [group["id"] for group in dictized["groups"]]
+
+ subjects = dictized.pop("relationships_as_subject")
+ objects = dictized.pop("relationships_as_object")
+
+ relationships = []
+ for rel in objects:
+ model = context['model']
+ swap_types = model.PackageRelationship.forward_to_reverse_type
+ type = swap_types(rel['type'])
+ relationships.append({'subject': api_fn(rel['object_package_id']),
+ 'type': type,
+ 'object': api_fn(rel['subject_package_id']),
+ 'comment': rel["comment"]})
+ for rel in subjects:
+ relationships.append({'subject': api_fn(rel['subject_package_id']),
+ 'type': rel['type'],
+ 'object': api_fn(rel['object_package_id']),
+ 'comment': rel["comment"]})
+
+ dictized['relationships'] = relationships
+
+ return dictized
+
+def vocabulary_dictize(vocabulary, context, include_datasets=False):
+ vocabulary_dict = d.table_dictize(vocabulary, context)
+ assert not vocabulary_dict.has_key('tags')
+
+ vocabulary_dict['tags'] = [tag_dictize(tag, context, include_datasets)
+ for tag in vocabulary.tags]
+ return vocabulary_dict
+
+def vocabulary_list_dictize(vocabulary_list, context):
+ return [vocabulary_dictize(vocabulary, context)
+ for vocabulary in vocabulary_list]
+
+def activity_dictize(activity, context):
+ activity_dict = d.table_dictize(activity, context)
+ return activity_dict
+
+def activity_list_dictize(activity_list, context):
+ return [activity_dictize(activity, context) for activity in activity_list]
+
+def activity_detail_dictize(activity_detail, context):
+ return d.table_dictize(activity_detail, context)
+
+def activity_detail_list_dictize(activity_detail_list, context):
+ return [activity_detail_dictize(activity_detail, context)
+ for activity_detail in activity_detail_list]
+
+
+def package_to_api1(pkg, context):
+ # DEPRICIATED set api_version in context and use package_to_api()
+ context['api_version'] = 1
+ return package_to_api(pkg, context)
+
+def package_to_api2(pkg, context):
+ # DEPRICIATED set api_version in context and use package_to_api()
+ context['api_version'] = 2
+ return package_to_api(pkg, context)
+
+def group_to_api1(group, context):
+ # DEPRICIATED set api_version in context and use group_to_api()
+ context['api_version'] = 1
+ return group_to_api(group, context)
+
+def group_to_api2(group, context):
+ # DEPRICIATED set api_version in context and use group_to_api()
+ context['api_version'] = 2
+ return group_to_api(group, context)
+
+def tag_to_api1(tag, context):
+ # DEPRICIATED set api_version in context and use tag_to_api()
+ context['api_version'] = 1
+ return tag_to_api(tag, context)
+
+def tag_to_api2(tag, context):
+ # DEPRICIATED set api_version in context and use tag_to_api()
+ context['api_version'] = 2
+ return tag_to_api(tag, context)
+
+def user_following_user_dictize(follower, context):
+ return d.table_dictize(follower, context)
+
+def user_following_dataset_dictize(follower, context):
+ return d.table_dictize(follower, context)
+
+def user_following_group_dictize(follower, context):
+ return d.table_dictize(follower, context)
+
+def resource_view_dictize(resource_view, context):
+ dictized = d.table_dictize(resource_view, context)
+ dictized.pop('order')
+ config = dictized.pop('config', {})
+ dictized.update(config)
+ resource = context['model'].Resource.get(resource_view.resource_id)
+ package_id = resource.package_id
+ dictized['package_id'] = package_id
+ return dictized
+
+def resource_view_list_dictize(resource_views, context):
+ resource_view_dicts = []
+ for view in resource_views:
+ resource_view_dicts.append(resource_view_dictize(view, context))
+ return resource_view_dicts
+
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/dictization/model_save.py b/venv/lib/python2.7/site-packages/ckan/lib/dictization/model_save.py
new file mode 100644
index 00000000..454159f4
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/dictization/model_save.py
@@ -0,0 +1,614 @@
+# encoding: utf-8
+
+import datetime
+import uuid
+import logging
+
+from sqlalchemy.orm import class_mapper
+from six import string_types
+
+import ckan.lib.dictization as d
+import ckan.lib.helpers as h
+import ckan.authz as authz
+
+log = logging.getLogger(__name__)
+
+
+def resource_dict_save(res_dict, context):
+
+ model = context["model"]
+ session = context["session"]
+
+ id = res_dict.get("id")
+ obj = None
+ if id:
+ obj = session.query(model.Resource).get(id)
+ if not obj:
+ new = True
+ obj = model.Resource()
+ else:
+ new = False
+
+ table = class_mapper(model.Resource).mapped_table
+ fields = [field.name for field in table.c]
+
+ # Strip the full url for resources of type 'upload'
+ if res_dict.get('url') and res_dict.get('url_type') == u'upload':
+ res_dict['url'] = res_dict['url'].rsplit('/')[-1]
+
+ # Resource extras not submitted will be removed from the existing extras
+ # dict
+ new_extras = {}
+ for key, value in res_dict.iteritems():
+ if isinstance(value, list):
+ continue
+ if key in ('extras', 'revision_timestamp', 'tracking_summary'):
+ continue
+ if key in fields:
+ if isinstance(getattr(obj, key), datetime.datetime):
+ if getattr(obj, key).isoformat() == value:
+ continue
+ if key == 'last_modified' and not new:
+ obj.url_changed = True
+ if key == 'url' and not new and obj.url != value:
+ obj.url_changed = True
+ setattr(obj, key, value)
+ else:
+ # resources save extras directly onto the object, instead
+ # of in a separate extras field like packages and groups
+ new_extras[key] = value
+
+ obj.state = u'active'
+ obj.extras = new_extras
+
+ session.add(obj)
+ return obj
+
+def package_resource_list_save(res_dicts, package, context):
+ allow_partial_update = context.get("allow_partial_update", False)
+ if res_dicts is None and allow_partial_update:
+ return
+
+ resource_list = package.resources_all
+ old_list = package.resources_all[:]
+
+ obj_list = []
+ for res_dict in res_dicts or []:
+ if not u'package_id' in res_dict or not res_dict[u'package_id']:
+ res_dict[u'package_id'] = package.id
+ obj = resource_dict_save(res_dict, context)
+ obj_list.append(obj)
+
+ # Set the package's resources. resource_list is an ORM relation - the
+ # package's resources. If we didn't have the slice operator "[:]" then it
+ # would reassign the variable "resource_list" to be the obj_list. But with
+ # the slice operator it changes the contents of the relation, setting the
+ # package's resources.
+ # At the table level, for each resource in the obj_list, its
+ # resource.package_id is changed to this package (which is needed for new
+ # resources), and every resource.position is set to ascending integers,
+ # according to their ordering in the obj_list.
+ resource_list[:] = obj_list
+
+ # Mark any left-over resources as deleted
+ for resource in set(old_list) - set(obj_list):
+ resource.state = 'deleted'
+ resource_list.append(resource)
+
+
+def package_extras_save(extra_dicts, obj, context):
+ allow_partial_update = context.get("allow_partial_update", False)
+ if extra_dicts is None and allow_partial_update:
+ return
+
+ model = context["model"]
+ session = context["session"]
+
+ extras_list = obj.extras_list
+ old_extras = dict((extra.key, extra) for extra in extras_list)
+
+ new_extras = {}
+ for extra_dict in extra_dicts or []:
+ if extra_dict.get("deleted"):
+ continue
+
+ if extra_dict['value'] is None:
+ pass
+ else:
+ new_extras[extra_dict["key"]] = extra_dict["value"]
+ #new
+ for key in set(new_extras.keys()) - set(old_extras.keys()):
+ state = 'active'
+ extra = model.PackageExtra(state=state, key=key, value=new_extras[key])
+ session.add(extra)
+ extras_list.append(extra)
+ #changed
+ for key in set(new_extras.keys()) & set(old_extras.keys()):
+ extra = old_extras[key]
+ if new_extras[key] == extra.value and extra.state != 'deleted':
+ continue
+ state = 'active'
+ extra.value = new_extras[key]
+ extra.state = state
+ session.add(extra)
+ #deleted
+ for key in set(old_extras.keys()) - set(new_extras.keys()):
+ extra = old_extras[key]
+ if extra.state == 'deleted':
+ continue
+ state = 'deleted'
+ extra.state = state
+
+def package_tag_list_save(tag_dicts, package, context):
+ allow_partial_update = context.get("allow_partial_update", False)
+ if tag_dicts is None and allow_partial_update:
+ return
+
+ model = context["model"]
+ session = context["session"]
+
+ tag_package_tag = dict((package_tag.tag, package_tag)
+ for package_tag in
+ package.package_tag_all)
+
+ tag_package_tag_inactive = {tag: pt for tag,pt in tag_package_tag.items() if
+ pt.state in ['deleted']}
+
+ tag_name_vocab = set()
+ tags = set()
+ for tag_dict in tag_dicts or []:
+ if (tag_dict.get('name'), tag_dict.get('vocabulary_id')) not in tag_name_vocab:
+ tag_obj = d.table_dict_save(tag_dict, model.Tag, context)
+ tags.add(tag_obj)
+ tag_name_vocab.add((tag_obj.name, tag_obj.vocabulary_id))
+
+ # 3 cases
+ # case 1: currently active but not in new list
+ for tag in set(tag_package_tag.keys()) - tags:
+ package_tag = tag_package_tag[tag]
+ package_tag.state = 'deleted'
+
+ # case 2: in new list but never used before
+ for tag in tags - set(tag_package_tag.keys()):
+ state = 'active'
+ package_tag_obj = model.PackageTag(package, tag, state)
+ session.add(package_tag_obj)
+ tag_package_tag[tag] = package_tag_obj
+
+ # case 3: in new list and already used but in deleted state
+ for tag in tags.intersection(set(tag_package_tag_inactive.keys())):
+ state = 'active'
+ package_tag = tag_package_tag[tag]
+ package_tag.state = state
+
+ package.package_tag_all[:] = tag_package_tag.values()
+
+def package_membership_list_save(group_dicts, package, context):
+
+ allow_partial_update = context.get("allow_partial_update", False)
+ if group_dicts is None and allow_partial_update:
+ return
+
+ capacity = 'public'
+ model = context["model"]
+ session = context["session"]
+ user = context.get('user')
+
+ members = session.query(model.Member) \
+ .filter(model.Member.table_id == package.id) \
+ .filter(model.Member.capacity != 'organization')
+
+ group_member = dict((member.group, member)
+ for member in
+ members)
+ groups = set()
+ for group_dict in group_dicts or []:
+ id = group_dict.get("id")
+ name = group_dict.get("name")
+ capacity = group_dict.get("capacity", "public")
+ if capacity == 'organization':
+ continue
+ if id:
+ group = session.query(model.Group).get(id)
+ else:
+ group = session.query(model.Group).filter_by(name=name).first()
+ if group:
+ groups.add(group)
+
+ ## need to flush so we can get out the package id
+ model.Session.flush()
+
+ # Remove any groups we are no longer in
+ for group in set(group_member.keys()) - groups:
+ member_obj = group_member[group]
+ if member_obj and member_obj.state == 'deleted':
+ continue
+ if authz.has_user_permission_for_group_or_org(
+ member_obj.group_id, user, 'read'):
+ member_obj.capacity = capacity
+ member_obj.state = 'deleted'
+ session.add(member_obj)
+
+ # Add any new groups
+ for group in groups:
+ member_obj = group_member.get(group)
+ if member_obj and member_obj.state == 'active':
+ continue
+ if authz.has_user_permission_for_group_or_org(
+ group.id, user, 'read'):
+ member_obj = group_member.get(group)
+ if member_obj:
+ member_obj.capacity = capacity
+ member_obj.state = 'active'
+ else:
+ member_obj = model.Member(table_id=package.id,
+ table_name='package',
+ group=group,
+ capacity=capacity,
+ group_id=group.id,
+ state = 'active')
+ session.add(member_obj)
+
+
+def relationship_list_save(relationship_dicts, package, attr, context):
+
+ allow_partial_update = context.get("allow_partial_update", False)
+ if relationship_dicts is None and allow_partial_update:
+ return
+
+ model = context["model"]
+ session = context["session"]
+
+ relationship_list = getattr(package, attr)
+ old_list = relationship_list[:]
+
+ relationships = []
+ for relationship_dict in relationship_dicts or []:
+ obj = d.table_dict_save(relationship_dict,
+ model.PackageRelationship, context)
+ relationships.append(obj)
+
+ relationship_list[:] = relationships
+
+ for relationship in set(old_list) - set(relationship_list):
+ relationship.state = 'deleted'
+ relationship_list.append(relationship)
+
+def package_dict_save(pkg_dict, context):
+ model = context["model"]
+ package = context.get("package")
+ allow_partial_update = context.get("allow_partial_update", False)
+ if package:
+ pkg_dict["id"] = package.id
+ Package = model.Package
+
+ if 'metadata_created' in pkg_dict:
+ del pkg_dict['metadata_created']
+ if 'metadata_modified' in pkg_dict:
+ del pkg_dict['metadata_modified']
+
+ pkg = d.table_dict_save(pkg_dict, Package, context)
+
+ if not pkg.id:
+ pkg.id = str(uuid.uuid4())
+
+ package_resource_list_save(pkg_dict.get("resources"), pkg, context)
+ package_tag_list_save(pkg_dict.get("tags"), pkg, context)
+ package_membership_list_save(pkg_dict.get("groups"), pkg, context)
+
+ # relationships are not considered 'part' of the package, so only
+ # process this if the key is provided
+ if 'relationships_as_subject' in pkg_dict:
+ subjects = pkg_dict.get('relationships_as_subject')
+ relationship_list_save(subjects, pkg, 'relationships_as_subject', context)
+ if 'relationships_as_object' in pkg_dict:
+ objects = pkg_dict.get('relationships_as_object')
+ relationship_list_save(objects, pkg, 'relationships_as_object', context)
+
+ extras = package_extras_save(pkg_dict.get("extras"), pkg, context)
+
+ return pkg
+
+def group_member_save(context, group_dict, member_table_name):
+ model = context["model"]
+ session = context["session"]
+ group = context['group']
+ entity_list = group_dict.get(member_table_name, None)
+
+ if entity_list is None:
+ if context.get('allow_partial_update', False):
+ return {'added': [], 'removed': []}
+ else:
+ entity_list = []
+
+ entities = {}
+ Member = model.Member
+
+ classname = member_table_name[:-1].capitalize()
+ if classname == 'Organization':
+ # Organizations use the model.Group class
+ classname = 'Group'
+ ModelClass = getattr(model, classname)
+
+ for entity_dict in entity_list:
+ name_or_id = entity_dict.get('id') or entity_dict.get('name')
+ obj = ModelClass.get(name_or_id)
+ if obj and obj not in entities.values():
+ entities[(obj.id, entity_dict.get('capacity', 'public'))] = obj
+
+ members = session.query(Member).filter_by(
+ table_name=member_table_name[:-1],
+ group_id=group.id,
+ ).all()
+
+ processed = {
+ 'added': [],
+ 'removed': []
+ }
+
+ entity_member = dict(((member.table_id, member.capacity), member) for member in members)
+ for entity_id in set(entity_member.keys()) - set(entities.keys()):
+ if entity_member[entity_id].state != 'deleted':
+ processed['removed'].append(entity_id[0])
+ entity_member[entity_id].state = 'deleted'
+ session.add(entity_member[entity_id])
+
+ for entity_id in set(entity_member.keys()) & set(entities.keys()):
+ if entity_member[entity_id].state != 'active':
+ processed['added'].append(entity_id[0])
+ entity_member[entity_id].state = 'active'
+ session.add(entity_member[entity_id])
+
+ for entity_id in set(entities.keys()) - set(entity_member.keys()):
+ member = Member(group=group, group_id=group.id, table_id=entity_id[0],
+ table_name=member_table_name[:-1],
+ capacity=entity_id[1])
+ processed['added'].append(entity_id[0])
+ session.add(member)
+
+ return processed
+
+
+def group_dict_save(group_dict, context, prevent_packages_update=False):
+ from ckan.lib.search import rebuild
+
+ model = context["model"]
+ session = context["session"]
+ group = context.get("group")
+ allow_partial_update = context.get("allow_partial_update", False)
+
+ Group = model.Group
+ if group:
+ group_dict["id"] = group.id
+
+ group = d.table_dict_save(group_dict, Group, context)
+ if not group.id:
+ group.id = str(uuid.uuid4())
+
+ context['group'] = group
+
+ # Under the new org rules we do not want to be able to update datasets
+ # via group edit so we need a way to prevent this. It may be more
+ # sensible in future to send a list of allowed/disallowed updates for
+ # groups, users, tabs etc.
+ if not prevent_packages_update:
+ pkgs_edited = group_member_save(context, group_dict, 'packages')
+ else:
+ pkgs_edited = {
+ 'added': [],
+ 'removed': []
+ }
+ group_users_changed = group_member_save(context, group_dict, 'users')
+ group_groups_changed = group_member_save(context, group_dict, 'groups')
+ group_tags_changed = group_member_save(context, group_dict, 'tags')
+ log.debug('Group save membership changes - Packages: %r Users: %r '
+ 'Groups: %r Tags: %r', pkgs_edited, group_users_changed,
+ group_groups_changed, group_tags_changed)
+
+ extras = group_dict.get("extras", [])
+ new_extras = {i['key'] for i in extras}
+ if extras:
+ old_extras = group.extras
+ for key in set(old_extras) - new_extras:
+ del group.extras[key]
+ for x in extras:
+ if 'deleted' in x and x['key'] in old_extras:
+ del group.extras[x['key']]
+ continue
+ group.extras[x['key']] = x['value']
+
+ # We will get a list of packages that we have either added or
+ # removed from the group, and trigger a re-index.
+ package_ids = pkgs_edited['removed']
+ package_ids.extend( pkgs_edited['added'] )
+ if package_ids:
+ session.commit()
+ map( rebuild, package_ids )
+
+ return group
+
+
+def user_dict_save(user_dict, context):
+
+ model = context['model']
+ session = context['session']
+ user = context.get('user_obj')
+
+ User = model.User
+ if user:
+ user_dict['id'] = user.id
+
+ if 'password' in user_dict and not len(user_dict['password']):
+ del user_dict['password']
+
+ user = d.table_dict_save(user_dict, User, context)
+
+ return user
+
+
+def package_api_to_dict(api1_dict, context):
+
+ package = context.get("package")
+ api_version = context.get('api_version')
+ assert api_version, 'No api_version supplied in context'
+
+ dictized = {}
+
+ for key, value in api1_dict.iteritems():
+ new_value = value
+ if key == 'tags':
+ if isinstance(value, string_types):
+ new_value = [{"name": item} for item in value.split()]
+ else:
+ new_value = [{"name": item} for item in value]
+ if key == 'extras':
+ updated_extras = {}
+ if package:
+ updated_extras.update(package.extras)
+ updated_extras.update(value)
+
+ new_value = []
+
+ for extras_key, extras_value in updated_extras.iteritems():
+ new_value.append({"key": extras_key,
+ "value": extras_value})
+
+ if key == 'groups' and len(value):
+ if api_version == 1:
+ new_value = [{'name': item} for item in value]
+ else:
+ new_value = [{'id': item} for item in value]
+
+ dictized[key] = new_value
+
+ download_url = dictized.pop('download_url', None)
+ if download_url and not dictized.get('resources'):
+ dictized["resources"] = [{'url': download_url}]
+
+ download_url = dictized.pop('download_url', None)
+
+ return dictized
+
+def group_api_to_dict(api1_dict, context):
+
+ dictized = {}
+
+ for key, value in api1_dict.iteritems():
+ new_value = value
+ if key == 'packages':
+ new_value = [{"id": item} for item in value]
+ if key == 'extras':
+ new_value = [{"key": extra_key, "value": value[extra_key]}
+ for extra_key in value]
+ dictized[key] = new_value
+
+ return dictized
+
+def task_status_dict_save(task_status_dict, context):
+ model = context["model"]
+ task_status = context.get("task_status")
+ allow_partial_update = context.get("allow_partial_update", False)
+ if task_status:
+ task_status_dict["id"] = task_status.id
+
+ task_status = d.table_dict_save(task_status_dict, model.TaskStatus, context)
+ return task_status
+
+def activity_dict_save(activity_dict, context):
+
+ model = context['model']
+ session = context['session']
+ user_id = activity_dict['user_id']
+ object_id = activity_dict['object_id']
+ revision_id = activity_dict['revision_id']
+ activity_type = activity_dict['activity_type']
+ if activity_dict.has_key('data'):
+ data = activity_dict['data']
+ else:
+ data = None
+ activity_obj = model.Activity(user_id, object_id, revision_id,
+ activity_type, data)
+ session.add(activity_obj)
+
+ # TODO: Handle activity details.
+
+ return activity_obj
+
+def vocabulary_tag_list_save(new_tag_dicts, vocabulary_obj, context):
+ model = context['model']
+ session = context['session']
+
+ # First delete any tags not in new_tag_dicts.
+ for tag in vocabulary_obj.tags:
+ if tag.name not in [t['name'] for t in new_tag_dicts]:
+ tag.delete()
+ # Now add any new tags.
+ for tag_dict in new_tag_dicts:
+ current_tag_names = [tag.name for tag in vocabulary_obj.tags]
+ if tag_dict['name'] not in current_tag_names:
+ # Make sure the tag belongs to this vocab..
+ tag_dict['vocabulary_id'] = vocabulary_obj.id
+ # then add it.
+ tag_dict_save(tag_dict, {'model': model, 'session': session})
+
+def vocabulary_dict_save(vocabulary_dict, context):
+ model = context['model']
+ session = context['session']
+ vocabulary_name = vocabulary_dict['name']
+
+ vocabulary_obj = model.Vocabulary(vocabulary_name)
+ session.add(vocabulary_obj)
+
+ if vocabulary_dict.has_key('tags'):
+ vocabulary_tag_list_save(vocabulary_dict['tags'], vocabulary_obj,
+ context)
+
+ return vocabulary_obj
+
+def vocabulary_dict_update(vocabulary_dict, context):
+
+ model = context['model']
+ session = context['session']
+
+ vocabulary_obj = model.vocabulary.Vocabulary.get(vocabulary_dict['id'])
+
+ if vocabulary_dict.has_key('name'):
+ vocabulary_obj.name = vocabulary_dict['name']
+
+ if vocabulary_dict.has_key('tags'):
+ vocabulary_tag_list_save(vocabulary_dict['tags'], vocabulary_obj,
+ context)
+
+ return vocabulary_obj
+
+def tag_dict_save(tag_dict, context):
+ model = context['model']
+ tag = context.get('tag')
+ if tag:
+ tag_dict['id'] = tag.id
+ tag = d.table_dict_save(tag_dict, model.Tag, context)
+ return tag
+
+def follower_dict_save(data_dict, context, FollowerClass):
+ model = context['model']
+ session = context['session']
+ follower_obj = FollowerClass(
+ follower_id=model.User.get(context['user']).id,
+ object_id=data_dict['id'])
+ session.add(follower_obj)
+ return follower_obj
+
+
+def resource_view_dict_save(data_dict, context):
+ model = context['model']
+ resource_view = context.get('resource_view')
+ if resource_view:
+ data_dict['id'] = resource_view.id
+ config = {}
+ for key, value in data_dict.iteritems():
+ if key not in model.ResourceView.get_columns():
+ config[key] = value
+ data_dict['config'] = config
+
+
+ return d.table_dict_save(data_dict, model.ResourceView, context)
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/email_notifications.py b/venv/lib/python2.7/site-packages/ckan/lib/email_notifications.py
new file mode 100644
index 00000000..0552e797
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/email_notifications.py
@@ -0,0 +1,226 @@
+# encoding: utf-8
+
+'''
+Code for generating email notifications for users (e.g. email notifications for
+new activities in your dashboard activity stream) and emailing them to the
+users.
+
+'''
+import datetime
+import re
+
+import ckan.model as model
+import ckan.logic as logic
+import ckan.lib.base as base
+
+from ckan.common import ungettext, config
+
+
+def string_to_timedelta(s):
+ '''Parse a string s and return a standard datetime.timedelta object.
+
+ Handles days, hours, minutes, seconds, and microseconds.
+
+ Accepts strings in these formats:
+
+ 2 days
+ 14 days
+ 4:35:00 (hours, minutes and seconds)
+ 4:35:12.087465 (hours, minutes, seconds and microseconds)
+ 7 days, 3:23:34
+ 7 days, 3:23:34.087465
+ .087465 (microseconds only)
+
+ :raises ckan.logic.ValidationError: if the given string does not match any
+ of the recognised formats
+
+ '''
+ patterns = []
+ days_only_pattern = '(?P\d+)\s+day(s)?'
+ patterns.append(days_only_pattern)
+ hms_only_pattern = '(?P\d?\d):(?P\d\d):(?P\d\d)'
+ patterns.append(hms_only_pattern)
+ ms_only_pattern = '.(?P\d\d\d)(?P\d\d\d)'
+ patterns.append(ms_only_pattern)
+ hms_and_ms_pattern = hms_only_pattern + ms_only_pattern
+ patterns.append(hms_and_ms_pattern)
+ days_and_hms_pattern = '{0},\s+{1}'.format(days_only_pattern,
+ hms_only_pattern)
+ patterns.append(days_and_hms_pattern)
+ days_and_hms_and_ms_pattern = days_and_hms_pattern + ms_only_pattern
+ patterns.append(days_and_hms_and_ms_pattern)
+
+ for pattern in patterns:
+ match = re.match('^{0}$'.format(pattern), s)
+ if match:
+ break
+
+ if not match:
+ raise logic.ValidationError('Not a valid time: {0}'.format(s))
+
+ gd = match.groupdict()
+ days = int(gd.get('days', '0'))
+ hours = int(gd.get('hours', '0'))
+ minutes = int(gd.get('minutes', '0'))
+ seconds = int(gd.get('seconds', '0'))
+ milliseconds = int(gd.get('milliseconds', '0'))
+ microseconds = int(gd.get('microseconds', '0'))
+ delta = datetime.timedelta(days=days, hours=hours, minutes=minutes,
+ seconds=seconds, milliseconds=milliseconds,
+ microseconds=microseconds)
+ return delta
+
+
+def _notifications_for_activities(activities, user_dict):
+ '''Return one or more email notifications covering the given activities.
+
+ This function handles grouping multiple activities into a single digest
+ email.
+
+ :param activities: the activities to consider
+ :type activities: list of activity dicts like those returned by
+ ckan.logic.action.get.dashboard_activity_list()
+
+ :returns: a list of email notifications
+ :rtype: list of dicts each with keys 'subject' and 'body'
+
+ '''
+ if not activities:
+ return []
+
+ if not user_dict.get('activity_streams_email_notifications'):
+ return []
+
+ # We just group all activities into a single "new activity" email that
+ # doesn't say anything about _what_ new activities they are.
+ # TODO: Here we could generate some smarter content for the emails e.g.
+ # say something about the contents of the activities, or single out
+ # certain types of activity to be sent in their own individual emails,
+ # etc.
+ subject = ungettext(
+ "{n} new activity from {site_title}",
+ "{n} new activities from {site_title}",
+ len(activities)).format(
+ site_title=config.get('ckan.site_title'),
+ n=len(activities))
+ body = base.render(
+ 'activity_streams/activity_stream_email_notifications.text',
+ extra_vars={'activities': activities})
+ notifications = [{
+ 'subject': subject,
+ 'body': body
+ }]
+
+ return notifications
+
+
+def _notifications_from_dashboard_activity_list(user_dict, since):
+ '''Return any email notifications from the given user's dashboard activity
+ list since `since`.
+
+ '''
+ # Get the user's dashboard activity stream.
+ context = {'model': model, 'session': model.Session,
+ 'user': user_dict['id']}
+ activity_list = logic.get_action('dashboard_activity_list')(context, {})
+
+ # Filter out the user's own activities., so they don't get an email every
+ # time they themselves do something (we are not Trac).
+ activity_list = [activity for activity in activity_list
+ if activity['user_id'] != user_dict['id']]
+
+ # Filter out the old activities.
+ strptime = datetime.datetime.strptime
+ fmt = '%Y-%m-%dT%H:%M:%S.%f'
+ activity_list = [activity for activity in activity_list
+ if strptime(activity['timestamp'], fmt) > since]
+
+ return _notifications_for_activities(activity_list, user_dict)
+
+
+# A list of functions that provide email notifications for users from different
+# sources. Add to this list if you want to implement a new source of email
+# notifications.
+_notifications_functions = [
+ _notifications_from_dashboard_activity_list,
+ ]
+
+
+def get_notifications(user_dict, since):
+ '''Return any email notifications for the given user since `since`.
+
+ For example email notifications about activity streams will be returned for
+ any activities the occurred since `since`.
+
+ :param user_dict: a dictionary representing the user, should contain 'id'
+ and 'name'
+ :type user_dict: dictionary
+
+ :param since: datetime after which to return notifications from
+ :rtype since: datetime.datetime
+
+ :returns: a list of email notifications
+ :rtype: list of dicts with keys 'subject' and 'body'
+
+ '''
+ notifications = []
+ for function in _notifications_functions:
+ notifications.extend(function(user_dict, since))
+ return notifications
+
+
+def send_notification(user, email_dict):
+ '''Email `email_dict` to `user`.'''
+ import ckan.lib.mailer
+
+ if not user.get('email'):
+ # FIXME: Raise an exception.
+ return
+
+ try:
+ ckan.lib.mailer.mail_recipient(user['display_name'], user['email'],
+ email_dict['subject'], email_dict['body'])
+ except ckan.lib.mailer.MailerException:
+ raise
+
+
+def get_and_send_notifications_for_user(user):
+
+ # Parse the email_notifications_since config setting, email notifications
+ # from longer ago than this time will not be sent.
+ email_notifications_since = config.get(
+ 'ckan.email_notifications_since', '2 days')
+ email_notifications_since = string_to_timedelta(
+ email_notifications_since)
+ email_notifications_since = (datetime.datetime.utcnow()
+ - email_notifications_since)
+
+ # FIXME: We are accessing model from lib here but I'm not sure what
+ # else to do unless we add a get_email_last_sent() logic function which
+ # would only be needed by this lib.
+ email_last_sent = model.Dashboard.get(user['id']).email_last_sent
+ activity_stream_last_viewed = (
+ model.Dashboard.get(user['id']).activity_stream_last_viewed)
+
+ since = max(email_notifications_since, email_last_sent,
+ activity_stream_last_viewed)
+
+ notifications = get_notifications(user, since)
+ # TODO: Handle failures from send_email_notification.
+ for notification in notifications:
+ send_notification(user, notification)
+
+ # FIXME: We are accessing model from lib here but I'm not sure what
+ # else to do unless we add a update_email_last_sent()
+ # logic function which would only be needed by this lib.
+ dash = model.Dashboard.get(user['id'])
+ dash.email_last_sent = datetime.datetime.utcnow()
+ model.repo.commit()
+
+
+def get_and_send_notifications_for_all_users():
+ context = {'model': model, 'session': model.Session, 'ignore_auth': True,
+ 'keep_email': True}
+ users = logic.get_action('user_list')(context, {})
+ for user in users:
+ get_and_send_notifications_for_user(user)
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/extract.py b/venv/lib/python2.7/site-packages/ckan/lib/extract.py
new file mode 100644
index 00000000..6a57ce66
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/extract.py
@@ -0,0 +1,42 @@
+# encoding: utf-8
+
+import re
+from jinja2.ext import babel_extract as extract_jinja2
+import lib.jinja_extensions
+from six import string_types
+
+jinja_extensions = '''
+ jinja2.ext.do, jinja2.ext.with_,
+ ckan.lib.jinja_extensions.SnippetExtension,
+ ckan.lib.jinja_extensions.CkanExtend,
+ ckan.lib.jinja_extensions.LinkForExtension,
+ ckan.lib.jinja_extensions.ResourceExtension,
+ ckan.lib.jinja_extensions.UrlForStaticExtension,
+ ckan.lib.jinja_extensions.UrlForExtension
+ '''
+
+def jinja2_cleaner(fileobj, *args, **kw):
+ # We want to format the messages correctly and intercepting here seems
+ # the best location
+ # add our custom tags
+ kw['options']['extensions'] = jinja_extensions
+
+ raw_extract = extract_jinja2(fileobj, *args, **kw)
+
+ for lineno, func, message, finder in raw_extract:
+
+ if isinstance(message, string_types):
+ message = lib.jinja_extensions.regularise_html(message)
+ elif message is not None:
+ message = (lib.jinja_extensions.regularise_html(message[0])
+ ,lib.jinja_extensions.regularise_html(message[1]))
+
+ yield lineno, func, message, finder
+
+
+def extract_ckan(fileobj, *args, **kw):
+ source = fileobj.read()
+ output = jinja2_cleaner(fileobj, *args, **kw)
+ # we've eaten the file so we need to get back to the start
+ fileobj.seek(0)
+ return output
diff --git a/venv/lib/python2.7/site-packages/ckan/lib/fanstatic_extensions.py b/venv/lib/python2.7/site-packages/ckan/lib/fanstatic_extensions.py
new file mode 100644
index 00000000..28ad0e2c
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/lib/fanstatic_extensions.py
@@ -0,0 +1,116 @@
+# encoding: utf-8
+
+import fanstatic.core as core
+
+
+class CkanCustomRenderer(object):
+ ''' Allows for in-line js and IE conditionals via fanstatic. '''
+ def __init__(self, script=None, renderer=None, condition=None,
+ other_browsers=False):
+ self.script = script
+ self.other_browsers = other_browsers
+ self.renderer = renderer
+ start = ''
+ end = ''
+ # IE conditionals
+ if condition:
+ start = ''
+ if other_browsers:
+ start += ''
+ end = ' [1, 6, 1] '''
+ import re
+ v_str = re.sub(r'[^0-9.]', '', v_str)
+ return [int(part) for part in v_str.split('.')]
+
+ @classmethod
+ def _check_ckan_version(cls, min_version=None, max_version=None):
+ '''Return ``True`` if the CKAN version is greater than or equal to
+ ``min_version`` and less than or equal to ``max_version``,
+ return ``False`` otherwise.
+
+ If no ``min_version`` is given, just check whether the CKAN version is
+ less than or equal to ``max_version``.
+
+ If no ``max_version`` is given, just check whether the CKAN version is
+ greater than or equal to ``min_version``.
+
+ :param min_version: the minimum acceptable CKAN version,
+ eg. ``'2.1'``
+ :type min_version: string
+
+ :param max_version: the maximum acceptable CKAN version,
+ eg. ``'2.3'``
+ :type max_version: string
+
+ '''
+ current = cls._version_str_2_list(cls.ckan.__version__)
+
+ if min_version:
+ min_required = cls._version_str_2_list(min_version)
+ if current < min_required:
+ return False
+ if max_version:
+ max_required = cls._version_str_2_list(max_version)
+ if current > max_required:
+ return False
+ return True
+
+ @classmethod
+ def _requires_ckan_version(cls, min_version, max_version=None):
+ '''Raise :py:exc:`~ckan.plugins.toolkit.CkanVersionException` if the
+ CKAN version is not greater than or equal to ``min_version`` and
+ less then or equal to ``max_version``.
+
+ If no ``max_version`` is given, just check whether the CKAN version is
+ greater than or equal to ``min_version``.
+
+ Plugins can call this function if they require a certain CKAN version,
+ other versions of CKAN will crash if a user tries to use the plugin
+ with them.
+
+ :param min_version: the minimum acceptable CKAN version,
+ eg. ``'2.1'``
+ :type min_version: string
+
+ :param max_version: the maximum acceptable CKAN version,
+ eg. ``'2.3'``
+ :type max_version: string
+
+ '''
+ from ckan.exceptions import CkanVersionException
+ if not cls._check_ckan_version(min_version=min_version,
+ max_version=max_version):
+ if not max_version:
+ error = 'Requires ckan version %s or higher' % min_version
+ else:
+ error = 'Requires ckan version between {0} and {1}'.format(
+ min_version,
+ max_version
+ )
+ raise CkanVersionException(error)
+
+ @classmethod
+ def _get_endpoint(cls):
+ """Returns tuple in format: (controller|blueprint, action|view).
+ """
+ import ckan.common as common
+ try:
+ # CKAN >= 2.8
+ endpoint = tuple(common.request.endpoint.split('.'))
+ except AttributeError:
+ try:
+ return common.c.controller, common.c.action
+ except AttributeError:
+ return (None, None)
+ # there are some routes('hello_world') that are not using blueprint
+ # For such case, let's assume that view function is a controller
+ # itself and action is None.
+ if len(endpoint) is 1:
+ return endpoint + (None,)
+ return endpoint
+
+ def __getattr__(self, name):
+ ''' return the function/object requested '''
+ if not self._toolkit:
+ self._initialize()
+ if name in self._toolkit:
+ return self._toolkit[name]
+ else:
+ if name == '__bases__':
+ return self.__class__.__bases__
+ raise AttributeError('`%s` not found in plugins toolkit' % name)
+
+ def __dir__(self):
+ if not self._toolkit:
+ self._initialize()
+ return sorted(self._toolkit.keys())
+
+
+# https://mail.python.org/pipermail/python-ideas/2012-May/014969.html
+sys.modules[__name__] = _Toolkit()
diff --git a/venv/lib/python2.7/site-packages/ckan/plugins/toolkit_sphinx_extension.py b/venv/lib/python2.7/site-packages/ckan/plugins/toolkit_sphinx_extension.py
new file mode 100644
index 00000000..6ab963d0
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/plugins/toolkit_sphinx_extension.py
@@ -0,0 +1,187 @@
+# encoding: utf-8
+
+'''A Sphinx extension to automatically document CKAN's crazy plugins toolkit,
+autodoc-style.
+
+Sphinx's autodoc extension can document modules or classes, but although it
+masquerades as a module CKAN's plugins toolkit is actually neither a module nor
+a class, it's an object-instance of a class, and it's an object with weird
+__getattr__ behavior too. Autodoc can't handle it, so we have this custom
+Sphinx extension to automate documenting it instead.
+
+This extension plugs into the reading phase of the Sphinx build. It intercepts
+the 'toolkit' document (extensions/plugins-toolkit.rst) after Sphinx has read
+the reStructuredText source from file. It modifies the source, adding in Sphinx
+directives for everything in the plugins toolkit, and then the Sphinx build
+continues as normal (just as if the generated reStructuredText had been entered
+into plugins-toolkit.rst manually before running Sphinx).
+
+'''
+import inspect
+import types
+
+import ckan.plugins.toolkit as toolkit
+
+
+def setup(app):
+ '''Setup this Sphinx extension. Called once when initializing Sphinx.
+
+ '''
+ # Connect to Sphinx's source-read event, the callback function will be
+ # called after each source file is read.
+ app.connect('source-read', source_read)
+
+
+def format_function(name, function, docstring=None):
+ '''Return a Sphinx .. function:: directive for the given function.
+
+ The directive includes the function's docstring if it has one.
+
+ :param name: the name to give to the function in the directive,
+ eg. 'get_converter'
+ :type name: string
+
+ :param function: the function itself
+ :type function: function
+
+ :param docstring: if given, use this instead of introspecting the function
+ to find its actual docstring
+ :type docstring: string
+
+ :returns: a Sphinx .. function:: directive for the function
+ :rtype: string
+
+ '''
+ # The template we'll use to render the Sphinx function directive.
+ template = ('.. py:function:: ckan.plugins.toolkit.{function}{args}\n'
+ '\n'
+ '{docstring}\n'
+ '\n')
+
+ # Get the arguments of the function, as a string like:
+ # "(foo, bar=None, ...)"
+ argstring = inspect.formatargspec(*inspect.getargspec(function))
+
+ docstring = docstring or inspect.getdoc(function)
+ if docstring is None:
+ docstring = ''
+ else:
+ # Indent the docstring by 3 spaces, as needed for the Sphinx directive.
+ docstring = '\n'.join([' ' + line for line in docstring.split('\n')])
+
+ return template.format(function=name, args=argstring, docstring=docstring)
+
+
+def format_class(name, class_, docstring=None):
+ '''Return a Sphinx .. class:: directive for the given class.
+
+ The directive includes the class's docstring if it has one.
+
+ :param name: the name to give to the class in the directive,
+ eg. 'DefaultDatasetForm'
+ :type name: string
+
+ :param class_: the class itself
+ :type class_: class
+
+ :param docstring: if given, use this instead of introspecting the class
+ to find its actual docstring
+ :type docstring: string
+
+ :returns: a Sphinx .. class:: directive for the class
+ :rtype: string
+
+ '''
+ # The template we'll use to render the Sphinx class directive.
+ template = ('.. py:class:: ckan.plugins.toolkit.{cls}\n'
+ '\n'
+ '{docstring}\n'
+ '\n')
+
+ docstring = docstring or inspect.getdoc(class_)
+ if docstring is None:
+ docstring = ''
+ else:
+ # Indent the docstring by 3 spaces, as needed for the Sphinx directive.
+ docstring = '\n'.join([' ' + line for line in docstring.split('\n')])
+
+ return template.format(cls=name, docstring=docstring)
+
+
+def format_object(name, object_, docstring=None):
+ '''Return a Sphinx .. attribute:: directive for the given object.
+
+ The directive includes the object's class's docstring if it has one.
+
+ :param name: the name to give to the object in the directive,
+ eg. 'request'
+ :type name: string
+
+ :param object_: the object itself
+ :type object_: object
+
+ :param docstring: if given, use this instead of introspecting the object
+ to find its actual docstring
+ :type docstring: string
+
+ :returns: a Sphinx .. attribute:: directive for the object
+ :rtype: string
+
+ '''
+ # The template we'll use to render the Sphinx attribute directive.
+ template = ('.. py:attribute:: ckan.plugins.toolkit.{obj}\n'
+ '\n'
+ '{docstring}\n'
+ '\n')
+
+ docstring = docstring or inspect.getdoc(object_)
+ if docstring is None:
+ docstring = ''
+ else:
+ # Indent the docstring by 3 spaces, as needed for the Sphinx directive.
+ docstring = '\n'.join([' ' + line for line in docstring.split('\n')])
+
+ return template.format(obj=name, docstring=docstring)
+
+
+def source_read(app, docname, source):
+ '''Transform the contents of plugins-toolkit.rst to contain reference docs.
+
+ '''
+ # We're only interested in the 'plugins-toolkit' doc (plugins-toolkit.rst).
+ if docname != 'extensions/plugins-toolkit':
+ return
+
+ source_ = ''
+ for name, thing in inspect.getmembers(toolkit):
+
+ # The plugins toolkit can override the docstrings of some of its
+ # members (e.g. things that are imported from third-party libraries)
+ # by putting custom docstrings in this docstring_overrides dict.
+ custom_docstring = toolkit.docstring_overrides.get(name)
+
+ if inspect.isfunction(thing):
+ source_ += format_function(name, thing, docstring=custom_docstring)
+ elif inspect.ismethod(thing):
+ # We document plugins toolkit methods as if they're functions. This
+ # is correct because the class ckan.plugins.toolkit._Toolkit
+ # actually masquerades as a module ckan.plugins.toolkit, and you
+ # call its methods as if they were functions.
+ source_ += format_function(name, thing, docstring=custom_docstring)
+ elif inspect.isclass(thing):
+ source_ += format_class(name, thing, docstring=custom_docstring)
+ elif isinstance(thing, types.ObjectType):
+ source_ += format_object(name, thing, docstring=custom_docstring)
+
+ else:
+ assert False, ("Someone added {name}:{thing} to the plugins "
+ "toolkit and this Sphinx extension doesn't know "
+ "how to document that yet. If you're that someone, "
+ "you need to add a new format_*() function for it "
+ "here or the docs won't build.".format(
+ name=name, thing=thing))
+
+ source[0] += source_
+
+ # This is useful for debugging the generated RST.
+ # open('/tmp/source', 'w').write(source[0])
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/activity-stream.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/activity-stream.js
new file mode 100644
index 00000000..16314f5a
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/activity-stream.js
@@ -0,0 +1,119 @@
+/* Activity stream
+ * Handle the loading more of activity items within actiivity streams
+ *
+ * Options
+ * - more: are there more items to load
+ * - context: what's the context for the ajax calls
+ * - id: what's the id of the context?
+ * - offset: what's the current offset?
+ */
+this.ckan.module('activity-stream', function($) {
+ return {
+ /* options object can be extended using data-module-* attributes */
+ options : {
+ more: null,
+ id: null,
+ context: null,
+ offset: null,
+ loading: false
+ },
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ $.proxyAll(this, /_on/);
+ var options = this.options;
+ options.more = (options.more == 'True');
+ this._onBuildLoadMore();
+ $(window).on('scroll', this._onScrollIntoView);
+ this._onScrollIntoView();
+ },
+
+ /* Function that tells if el is within the window viewpost
+ *
+ * Returns boolean
+ */
+ elementInViewport: function(el) {
+ var top = el.offsetTop;
+ var left = el.offsetLeft;
+ var width = el.offsetWidth;
+ var height = el.offsetHeight;
+ while(el.offsetParent) {
+ el = el.offsetParent;
+ top += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return (
+ top < (window.pageYOffset + window.innerHeight) &&
+ left < (window.pageXOffset + window.innerWidth) &&
+ (top + height) > window.pageYOffset &&
+ (left + width) > window.pageXOffset
+ );
+ },
+
+ /* Whenever the window scrolls check if the load more button
+ * exists, if it's in the view and we're not already loading.
+ * If all conditions are satisfied... fire a click event on
+ * the load more button.
+ *
+ * Returns nothing
+ */
+ _onScrollIntoView: function() {
+ var el = $('.load-more a', this.el);
+ if (el.length == 1) {
+ var in_viewport = this.elementInViewport(el[0]);
+ if (in_viewport && !this.options.loading) {
+ el.trigger('click');
+ }
+ }
+ },
+
+ /* If we are able to load more... then attach the ajax request
+ * to the load more button.
+ *
+ * Returns nothing
+ */
+ _onBuildLoadMore: function() {
+ var options = this.options;
+ if (options.more) {
+ $('.load-more', this.el).on('click', 'a', this._onLoadMoreClick);
+ options.offset = $('.item', this.el).length;
+ }
+ },
+
+ /* Fires when someone clicks the load more button
+ * ... and if not loading then make the API call to load
+ * more activities
+ *
+ * Returns nothing
+ */
+ _onLoadMoreClick: function (event) {
+ event.preventDefault();
+ var options = this.options;
+ if (!options.loading) {
+ options.loading = true;
+ $('.load-more a', this.el).html(this._('Loading...')).addClass('disabled');
+ this.sandbox.client.call('GET', options.context+'_activity_list_html', '?id='+options.id+'&offset='+options.offset, this._onActivitiesLoaded);
+ }
+ },
+
+ /* Callback for after the API call
+ *
+ * Returns nothing
+ */
+ _onActivitiesLoaded: function(json) {
+ var options = this.options;
+ var result = $(json.result);
+ options.more = ( result.data('module-more') == 'True' );
+ options.offset += 30;
+ $('.load-less', result).remove();
+ $('.load-more', this.el).remove();
+ $('li', result).appendTo(this.el);
+ this._onBuildLoadMore();
+ options.loading = false;
+ }
+
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/activity-stream.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/activity-stream.min.js
new file mode 100644
index 00000000..3fbe3c04
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/activity-stream.min.js
@@ -0,0 +1,2 @@
+this.ckan.module('activity-stream',function($){return{options:{more:null,id:null,context:null,offset:null,loading:false},initialize:function(){$.proxyAll(this,/_on/);var options=this.options;options.more=(options.more=='True');this._onBuildLoadMore();$(window).on('scroll',this._onScrollIntoView);this._onScrollIntoView();},elementInViewport:function(el){var top=el.offsetTop;var left=el.offsetLeft;var width=el.offsetWidth;var height=el.offsetHeight;while(el.offsetParent){el=el.offsetParent;top+=el.offsetTop;left+=el.offsetLeft;}
+return(top<(window.pageYOffset+window.innerHeight)&&left<(window.pageXOffset+window.innerWidth)&&(top+height)>window.pageYOffset&&(left+width)>window.pageXOffset);},_onScrollIntoView:function(){var el=$('.load-more a',this.el);if(el.length==1){var in_viewport=this.elementInViewport(el[0]);if(in_viewport&&!this.options.loading){el.trigger('click');}}},_onBuildLoadMore:function(){var options=this.options;if(options.more){$('.load-more',this.el).on('click','a',this._onLoadMoreClick);options.offset=$('.item',this.el).length;}},_onLoadMoreClick:function(event){event.preventDefault();var options=this.options;if(!options.loading){options.loading=true;$('.load-more a',this.el).html(this._('Loading...')).addClass('disabled');this.sandbox.client.call('GET',options.context+'_activity_list_html','?id='+options.id+'&offset='+options.offset,this._onActivitiesLoaded);}},_onActivitiesLoaded:function(json){var options=this.options;var result=$(json.result);options.more=(result.data('module-more')=='True');options.offset+=30;$('.load-less',result).remove();$('.load-more',this.el).remove();$('li',result).appendTo(this.el);this._onBuildLoadMore();options.loading=false;}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/api-info.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/api-info.js
new file mode 100644
index 00000000..0271e30e
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/api-info.js
@@ -0,0 +1,127 @@
+/* Loads the API Info snippet into a modal dialog. Retrieves the snippet
+ * url from the data-snippet-url on the module element.
+ *
+ * template - The url to the template to display in a modal.
+ *
+ * Examples
+ *
+ * API
+ *
+ */
+this.ckan.module('api-info', function (jQuery) {
+ return {
+
+ /* holds the loaded lightbox */
+ modal: null,
+
+ options: {
+ template: null
+ },
+
+ /* Sets up the API info module.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ jQuery.proxyAll(this, /_on/);
+
+ this.el.on('click', this._onClick);
+ this.el.button();
+ },
+
+ /* Displays a loading message in the button. If false is provided as an
+ * argument the message is reset.
+ *
+ * loading - Resets the message if false.
+ *
+ * Examples
+ *
+ * module.loading(); // Show
+ * module.loading(false); // Hide
+ *
+ * Returns nothing.
+ */
+ loading: function (loading) {
+ this.el.button(loading !== false ? 'loading' : 'reset');
+ },
+
+ /* Displays the API info box.
+ *
+ * Examples
+ *
+ * module.show()
+ *
+ * Returns nothing.
+ */
+ show: function () {
+ var sandbox = this.sandbox,
+ module = this;
+
+ if (this.modal) {
+ return this.modal.modal('show');
+ }
+
+ this.loadTemplate().done(function (html) {
+ module.modal = jQuery(html);
+ module.modal.find('.modal-header :header').append('× ');
+ module.modal.modal().appendTo(sandbox.body);
+ });
+ },
+
+ /* Hides the modal.
+ *
+ * Examples
+ *
+ * module.hide();
+ *
+ * Returns nothing.
+ */
+ hide: function () {
+ if (this.modal) {
+ this.modal.modal('hide');
+ }
+ },
+
+ /* Loads the template and returns a promise that on complete will
+ * receive the html content for the modal.
+ *
+ * Examples
+ *
+ * module.loadTemplate().then(onSuccess, onError);
+ *
+ * Returns a promise instance.
+ */
+ loadTemplate: function () {
+ if (!this.options.template) {
+ this.sandbox.notify(this._('There is no API data to load for this resource'));
+ return jQuery.Deferred().reject().promise();
+ }
+
+ if (!this.promise) {
+ this.loading();
+
+ // This should use sandbox.client!
+ this.promise = jQuery.get(this.options.template);
+ this.promise.then(this._onTemplateSuccess, this._onTemplateError);
+ }
+ return this.promise;
+ },
+
+ /* Event handler for clicking on the element */
+ _onClick: function (event) {
+ event.preventDefault();
+ this.show();
+ },
+
+ /* Success handler for when the template is loaded */
+ _onTemplateSuccess: function () {
+ this.loading(false);
+ },
+
+ /* error handler when the template fails to load */
+ _onTemplateError: function () {
+ this.loading(false);
+ this.sandbox.notify(this._('Failed to load data API information'));
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/api-info.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/api-info.min.js
new file mode 100644
index 00000000..87472dd6
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/api-info.min.js
@@ -0,0 +1,4 @@
+this.ckan.module('api-info',function(jQuery){return{modal:null,options:{template:null},initialize:function(){jQuery.proxyAll(this,/_on/);this.el.on('click',this._onClick);this.el.button();},loading:function(loading){this.el.button(loading!==false?'loading':'reset');},show:function(){var sandbox=this.sandbox,module=this;if(this.modal){return this.modal.modal('show');}
+this.loadTemplate().done(function(html){module.modal=jQuery(html);module.modal.find('.modal-header :header').append('× ');module.modal.modal().appendTo(sandbox.body);});},hide:function(){if(this.modal){this.modal.modal('hide');}},loadTemplate:function(){if(!this.options.template){this.sandbox.notify(this._('There is no API data to load for this resource'));return jQuery.Deferred().reject().promise();}
+if(!this.promise){this.loading();this.promise=jQuery.get(this.options.template);this.promise.then(this._onTemplateSuccess,this._onTemplateError);}
+return this.promise;},_onClick:function(event){event.preventDefault();this.show();},_onTemplateSuccess:function(){this.loading(false);},_onTemplateError:function(){this.loading(false);this.sandbox.notify(this._('Failed to load data API information'));}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/autocomplete.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/autocomplete.js
new file mode 100644
index 00000000..769f06e6
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/autocomplete.js
@@ -0,0 +1,275 @@
+/* An auto-complete module for select and input elements that can pull in
+ * a list of terms from an API endpoint (provided using data-module-source).
+ *
+ * source - A url pointing to an API autocomplete endpoint.
+ * interval - The interval between requests in milliseconds (default: 1000).
+ * items - The max number of items to display (default: 10)
+ * tags - Boolean attribute if true will create a tag input.
+ * key - A string of the key you want to be the form value to end up on
+ * from the ajax returned results
+ * label - A string of the label you want to appear within the dropdown for
+ * returned results
+ *
+ * Examples
+ *
+ * //
+ *
+ */
+this.ckan.module('autocomplete', function (jQuery) {
+ return {
+ /* Options for the module */
+ options: {
+ tags: false,
+ key: false,
+ label: false,
+ items: 10,
+ source: null,
+ interval: 300,
+ dropdownClass: '',
+ containerClass: ''
+ },
+
+ /* Sets up the module, binding methods, creating elements etc. Called
+ * internally by ckan.module.initialize();
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ jQuery.proxyAll(this, /_on/, /format/);
+ this.setupAutoComplete();
+ },
+
+ /* Sets up the auto complete plugin.
+ *
+ * Returns nothing.
+ */
+ setupAutoComplete: function () {
+ var settings = {
+ width: 'resolve',
+ formatResult: this.formatResult,
+ formatNoMatches: this.formatNoMatches,
+ formatInputTooShort: this.formatInputTooShort,
+ dropdownCssClass: this.options.dropdownClass,
+ containerCssClass: this.options.containerClass
+ };
+
+ // Different keys are required depending on whether the select is
+ // tags or generic completion.
+ if (!this.el.is('select')) {
+ if (this.options.tags) {
+ settings.tags = this._onQuery;
+ } else {
+ settings.query = this._onQuery;
+ settings.createSearchChoice = this.formatTerm;
+ }
+ settings.initSelection = this.formatInitialValue;
+ }
+ else {
+ if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) {
+ var ieversion=new Number(RegExp.$1);
+ if (ieversion<=7) {return}
+ }
+ }
+
+ var select2 = this.el.select2(settings).data('select2');
+
+ if (this.options.tags && select2 && select2.search) {
+ // find the "fake" input created by select2 and add the keypress event.
+ // This is not part of the plugins API and so may break at any time.
+ select2.search.on('keydown', this._onKeydown);
+ }
+
+ // This prevents Internet Explorer from causing a window.onbeforeunload
+ // even from firing unnecessarily
+ $('.select2-choice', select2.container).on('click', function() {
+ return false;
+ });
+
+ this._select2 = select2;
+ },
+
+ /* Looks up the completions for the current search term and passes them
+ * into the provided callback function.
+ *
+ * The results are formatted for use in the select2 autocomplete plugin.
+ *
+ * string - The term to search for.
+ * fn - A callback function.
+ *
+ * Examples
+ *
+ * module.getCompletions('cake', function (results) {
+ * results === {results: []}
+ * });
+ *
+ * Returns a jqXHR promise.
+ */
+ getCompletions: function (string, fn) {
+ var parts = this.options.source.split('?');
+ var end = parts.pop();
+ var source = parts.join('?') + encodeURIComponent(string) + end;
+ var client = this.sandbox.client;
+ var options = {
+ format: function(data) {
+ var completion_options = jQuery.extend(options, {objects: true});
+ return {
+ results: client.parseCompletions(data, completion_options)
+ }
+ },
+ key: this.options.key,
+ label: this.options.label
+ };
+
+ return client.getCompletions(source, options, fn);
+ },
+
+ /* Looks up the completions for the provided text but also provides a few
+ * optimisations. If there is no search term it will automatically set
+ * an empty array. Ajax requests will also be debounced to ensure that
+ * the server is not overloaded.
+ *
+ * string - The term to search for.
+ * fn - A callback function.
+ *
+ * Returns nothing.
+ */
+ lookup: function (string, fn) {
+ var module = this;
+
+ // Cache the last searched term otherwise we'll end up searching for
+ // old data.
+ this._lastTerm = string;
+
+ // Kills previous timeout
+ clearTimeout(this._debounced);
+
+ // OK, wipe the dropdown before we start ajaxing the completions
+ fn({results:[]});
+
+ if (string) {
+ // Set a timer to prevent the search lookup occurring too often.
+ this._debounced = setTimeout(function () {
+ var term = module._lastTerm;
+
+ // Cancel the previous request if it hasn't yet completed.
+ if (module._last && typeof module._last.abort == 'function') {
+ module._last.abort();
+ }
+
+ module._last = module.getCompletions(term, fn);
+ }, this.options.interval);
+
+ // This forces the ajax throbber to appear, because we've called the
+ // callback already and that hides the throbber
+ $('.select2-search input', this._select2.dropdown).addClass('select2-active');
+ }
+ },
+
+ /* Formatter for the select2 plugin that returns a string for use in the
+ * results list with the current term emboldened.
+ *
+ * state - The current object that is being rendered.
+ * container - The element the content will be added to (added in 3.0)
+ * query - The query object (added in select2 3.0).
+ *
+ *
+ * Returns a text string.
+ */
+ formatResult: function (state, container, query) {
+ var term = this._lastTerm || null; // same as query.term
+
+ if (container) {
+ // Append the select id to the element for styling.
+ container.attr('data-value', state.id);
+ }
+
+ return state.text.split(term).join(term && term.bold());
+ },
+
+ /* Formatter for the select2 plugin that returns a string used when
+ * the filter has no matches.
+ *
+ * Returns a text string.
+ */
+ formatNoMatches: function (term) {
+ return !term ? this._('Start typing…') : this._('No matches found');
+ },
+
+ /* Formatter used by the select2 plugin that returns a string when the
+ * input is too short.
+ *
+ * Returns a string.
+ */
+ formatInputTooShort: function (term, min) {
+ return this.ngettext(
+ 'Input is too short, must be at least one character',
+ 'Input is too short, must be at least %(num)d characters',
+ min
+ );
+ },
+
+ /* Takes a string and converts it into an object used by the select2 plugin.
+ *
+ * term - The term to convert.
+ *
+ * Returns an object for use in select2.
+ */
+ formatTerm: function (term) {
+ term = jQuery.trim(term || '');
+
+ // Need to replace comma with a unicode character to trick the plugin
+ // as it won't split this into multiple items.
+ return {id: term.replace(/,/g, '\u002C'), text: term};
+ },
+
+ /* Callback function that parses the initial field value.
+ *
+ * element - The initialized input element wrapped in jQuery.
+ * callback - A callback to run once the formatting is complete.
+ *
+ * Returns a term object or an array depending on the type.
+ */
+ formatInitialValue: function (element, callback) {
+ var value = jQuery.trim(element.val() || '');
+ var formatted;
+
+ if (this.options.tags) {
+ formatted = jQuery.map(value.split(","), this.formatTerm);
+ } else {
+ formatted = this.formatTerm(value);
+ }
+
+ // Select2 v3.0 supports a callback for async calls.
+ if (typeof callback === 'function') {
+ callback(formatted);
+ }
+
+ return formatted;
+ },
+
+ /* Callback triggered when the select2 plugin needs to make a request.
+ *
+ * Returns nothing.
+ */
+ _onQuery: function (options) {
+ if (options) {
+ this.lookup(options.term, options.callback);
+ }
+ },
+
+ /* Called when a key is pressed. If the key is a comma we block it and
+ * then simulate pressing return.
+ *
+ * Returns nothing.
+ */
+ _onKeydown: function (event) {
+ if (typeof event.key !== 'undefined' ? event.key === ',' : event.which === 188) {
+ event.preventDefault();
+ setTimeout(function () {
+ var e = jQuery.Event("keydown", { which: 13 });
+ jQuery(event.target).trigger(e);
+ }, 10);
+ }
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/autocomplete.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/autocomplete.min.js
new file mode 100644
index 00000000..06219fb5
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/autocomplete.min.js
@@ -0,0 +1,9 @@
+this.ckan.module('autocomplete',function(jQuery){return{options:{tags:false,key:false,label:false,items:10,source:null,interval:300,dropdownClass:'',containerClass:''},initialize:function(){jQuery.proxyAll(this,/_on/,/format/);this.setupAutoComplete();},setupAutoComplete:function(){var settings={width:'resolve',formatResult:this.formatResult,formatNoMatches:this.formatNoMatches,formatInputTooShort:this.formatInputTooShort,dropdownCssClass:this.options.dropdownClass,containerCssClass:this.options.containerClass};if(!this.el.is('select')){if(this.options.tags){settings.tags=this._onQuery;}else{settings.query=this._onQuery;settings.createSearchChoice=this.formatTerm;}
+settings.initSelection=this.formatInitialValue;}
+else{if(/MSIE (\d+\.\d+);/.test(navigator.userAgent)){var ieversion=new Number(RegExp.$1);if(ieversion<=7){return}}}
+var select2=this.el.select2(settings).data('select2');if(this.options.tags&&select2&&select2.search){select2.search.on('keydown',this._onKeydown);}
+$('.select2-choice',select2.container).on('click',function(){return false;});this._select2=select2;},getCompletions:function(string,fn){var parts=this.options.source.split('?');var end=parts.pop();var source=parts.join('?')+encodeURIComponent(string)+end;var client=this.sandbox.client;var options={format:function(data){var completion_options=jQuery.extend(options,{objects:true});return{results:client.parseCompletions(data,completion_options)}},key:this.options.key,label:this.options.label};return client.getCompletions(source,options,fn);},lookup:function(string,fn){var module=this;this._lastTerm=string;clearTimeout(this._debounced);fn({results:[]});if(string){this._debounced=setTimeout(function(){var term=module._lastTerm;if(module._last&&typeof module._last.abort=='function'){module._last.abort();}
+module._last=module.getCompletions(term,fn);},this.options.interval);$('.select2-search input',this._select2.dropdown).addClass('select2-active');}},formatResult:function(state,container,query){var term=this._lastTerm||null;if(container){container.attr('data-value',state.id);}
+return state.text.split(term).join(term&&term.bold());},formatNoMatches:function(term){return!term?this._('Start typing…'):this._('No matches found');},formatInputTooShort:function(term,min){return this.ngettext('Input is too short, must be at least one character','Input is too short, must be at least %(num)d characters',min);},formatTerm:function(term){term=jQuery.trim(term||'');return{id:term.replace(/,/g,'\u002C'),text:term};},formatInitialValue:function(element,callback){var value=jQuery.trim(element.val()||'');var formatted;if(this.options.tags){formatted=jQuery.map(value.split(","),this.formatTerm);}else{formatted=this.formatTerm(value);}
+if(typeof callback==='function'){callback(formatted);}
+return formatted;},_onQuery:function(options){if(options){this.lookup(options.term,options.callback);}},_onKeydown:function(event){if(typeof event.key!=='undefined'?event.key===',':event.which===188){event.preventDefault();setTimeout(function(){var e=jQuery.Event("keydown",{which:13});jQuery(event.target).trigger(e);},10);}}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/basic-form.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/basic-form.js
new file mode 100644
index 00000000..37915a61
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/basic-form.js
@@ -0,0 +1,24 @@
+this.ckan.module('basic-form', function (jQuery) {
+ return {
+ initialize: function () {
+ var message = this._('There are unsaved modifications to this form');
+
+ $.proxyAll(this, /_on/);
+
+ this.el.incompleteFormWarning(message);
+
+ // Disable the submit button on form submit, to prevent multiple
+ // consecutive form submissions.
+ this.el.on('submit', this._onSubmit);
+ },
+ _onSubmit: function () {
+
+ // The button is not disabled immediately so that its value can be sent
+ // the first time the form is submitted, because the "save" field is
+ // used in the backend.
+ setTimeout(function() {
+ this.el.find('button[name="save"]').attr('disabled', true);
+ }.bind(this), 0);
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/basic-form.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/basic-form.min.js
new file mode 100644
index 00000000..0f9142f4
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/basic-form.min.js
@@ -0,0 +1 @@
+this.ckan.module('basic-form',function(jQuery){return{initialize:function(){var message=this._('There are unsaved modifications to this form');$.proxyAll(this,/_on/);this.el.incompleteFormWarning(message);this.el.on('submit',this._onSubmit);},_onSubmit:function(){setTimeout(function(){this.el.find('button[name="save"]').attr('disabled',true);}.bind(this),0);}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/confirm-action.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/confirm-action.js
new file mode 100644
index 00000000..8afd65a8
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/confirm-action.js
@@ -0,0 +1,128 @@
+this.ckan.module('confirm-action', function (jQuery) {
+ return {
+ /* An object of module options */
+ options: {
+ /* Content can be overriden by setting data-module-content to a
+ * *translated* string inside the template, e.g.
+ *
+ *
+ * {{ _('Delete') }}
+ *
+ */
+ content: '',
+
+ /* This is part of the old i18n system and is kept for backwards-
+ * compatibility for templates which set the content via the
+ * `i18n.content` attribute instead of via the `content` attribute
+ * as described above.
+ */
+ i18n: {
+ content: '',
+ },
+
+ template: [
+ ''
+ ].join('\n')
+ },
+
+ /* Sets up the event listeners for the object. Called internally by
+ * module.createInstance().
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ jQuery.proxyAll(this, /_on/);
+ this.el.on('click', this._onClick);
+ },
+
+ /* Presents the user with a confirm dialogue to ensure that they wish to
+ * continue with the current action.
+ *
+ * Examples
+ *
+ * jQuery('.delete').click(function () {
+ * module.confirm();
+ * });
+ *
+ * Returns nothing.
+ */
+ confirm: function () {
+ this.sandbox.body.append(this.createModal());
+ this.modal.modal('show');
+
+ // Center the modal in the middle of the screen.
+ this.modal.css({
+ 'margin-top': this.modal.height() * -0.5,
+ 'top': '50%'
+ });
+ },
+
+ /* Performs the action for the current item.
+ *
+ * Returns nothing.
+ */
+ performAction: function () {
+ // create a form and submit it to confirm the deletion
+ var form = jQuery('', {
+ action: this.el.attr('href'),
+ method: 'POST'
+ });
+ form.appendTo('body').submit();
+ },
+
+ /* Creates the modal dialog, attaches event listeners and localised
+ * strings.
+ *
+ * Returns the newly created element.
+ */
+ createModal: function () {
+ if (!this.modal) {
+ var element = this.modal = jQuery(this.options.template);
+ element.on('click', '.btn-primary', this._onConfirmSuccess);
+ element.on('click', '.btn-cancel', this._onConfirmCancel);
+ element.modal({show: false});
+
+ element.find('.modal-title').text(this._('Please Confirm Action'));
+ var content = this.options.content ||
+ this.options.i18n.content || /* Backwards-compatibility */
+ this._('Are you sure you want to perform this action?');
+ element.find('.modal-body').text(content);
+ element.find('.btn-primary').text(this._('Confirm'));
+ element.find('.btn-cancel').text(this._('Cancel'));
+ }
+ return this.modal;
+ },
+
+ /* Event handler that displays the confirm dialog */
+ _onClick: function (event) {
+ event.preventDefault();
+ this.confirm();
+ },
+
+ /* Event handler for the success event */
+ _onConfirmSuccess: function (event) {
+ this.performAction();
+ },
+
+ /* Event handler for the cancel event */
+ _onConfirmCancel: function (event) {
+ this.modal.modal('hide');
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/confirm-action.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/confirm-action.min.js
new file mode 100644
index 00000000..900e8762
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/confirm-action.min.js
@@ -0,0 +1,2 @@
+this.ckan.module('confirm-action',function(jQuery){return{options:{content:'',i18n:{content:'',},template:[''].join('\n')},initialize:function(){jQuery.proxyAll(this,/_on/);this.el.on('click',this._onClick);},confirm:function(){this.sandbox.body.append(this.createModal());this.modal.modal('show');this.modal.css({'margin-top':this.modal.height()*-0.5,'top':'50%'});},performAction:function(){var form=jQuery('',{action:this.el.attr('href'),method:'POST'});form.appendTo('body').submit();},createModal:function(){if(!this.modal){var element=this.modal=jQuery(this.options.template);element.on('click','.btn-primary',this._onConfirmSuccess);element.on('click','.btn-cancel',this._onConfirmCancel);element.modal({show:false});element.find('.modal-title').text(this._('Please Confirm Action'));var content=this.options.content||this.options.i18n.content||this._('Are you sure you want to perform this action?');element.find('.modal-body').text(content);element.find('.btn-primary').text(this._('Confirm'));element.find('.btn-cancel').text(this._('Cancel'));}
+return this.modal;},_onClick:function(event){event.preventDefault();this.confirm();},_onConfirmSuccess:function(event){this.performAction();},_onConfirmCancel:function(event){this.modal.modal('hide');}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/custom-fields.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/custom-fields.js
new file mode 100644
index 00000000..189b0031
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/custom-fields.js
@@ -0,0 +1,99 @@
+/* Module for working with multiple custom field inputs. This will create
+ * a new field when the user enters text into the last field key. It also
+ * gives a visual indicator when fields are removed by disabling them.
+ *
+ * See the snippets/custom_form_fields.html for an example.
+ */
+this.ckan.module('custom-fields', function (jQuery) {
+ return {
+ options: {
+ /* The selector used for each custom field wrapper */
+ fieldSelector: '.control-custom'
+ },
+
+ /* Initializes the module and attaches custom event listeners. This
+ * is called internally by ckan.module.initialize().
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ jQuery.proxyAll(this, /_on/);
+
+ var delegated = this.options.fieldSelector + ':last input:first';
+ this.el.on('change', delegated, this._onChange);
+ this.el.on('change', ':checkbox', this._onRemove);
+ },
+
+ /* Creates a new field and appends it to the list. This currently works by
+ * cloning and erasing an existing input rather than using a template. In
+ * future using a template might be more appropriate.
+ *
+ * element - Another custom field element to wrap.
+ *
+ * Returns nothing.
+ */
+ newField: function (element) {
+ this.el.append(this.cloneField(element));
+ },
+
+ /* Clones the provided element, wipes it's content and increments it's
+ * for, id and name fields (if possible).
+ *
+ * current - A custom field to clone.
+ *
+ * Returns a newly created custom field element.
+ */
+ cloneField: function (current) {
+ return this.resetField(jQuery(current).clone());
+ },
+
+ /* Wipes the contents of the field provided and increments it's name, id
+ * and for attributes.
+ *
+ * field - A custom field to wipe.
+ *
+ * Returns the wiped element.
+ */
+ resetField: function (field) {
+ function increment(index, string) {
+ return (string || '').replace(/\d+/, function (int) { return 1 + parseInt(int, 10); });
+ }
+
+ var input = field.find(':input');
+ input.val('').attr('id', increment).attr('name', increment);
+
+ var label = field.find('label');
+ label.text(increment).attr('for', increment);
+
+ return field;
+ },
+
+ /* Disables the provided field and input elements. Can be re-enabled by
+ * passing false as the second argument.
+ *
+ * field - The field to disable.
+ * disable - If false re-enables the element.
+ *
+ * Returns nothing.
+ */
+ disableField: function (field, disable) {
+ field.toggleClass('disabled', disable !== false);
+ },
+
+ /* Event handler that fires when the last key in the custom field block
+ * changes.
+ */
+ _onChange: function (event) {
+ if (event.target.value !== '') {
+ var parent = jQuery(event.target).parents(this.options.fieldSelector);
+ this.newField(parent);
+ }
+ },
+
+ /* Event handler called when the remove checkbox is checked */
+ _onRemove: function (event) {
+ var parent = jQuery(event.target).parents(this.options.fieldSelector);
+ this.disableField(parent, event.target.checked);
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/custom-fields.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/custom-fields.min.js
new file mode 100644
index 00000000..9dc4ac8b
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/custom-fields.min.js
@@ -0,0 +1,2 @@
+this.ckan.module('custom-fields',function(jQuery){return{options:{fieldSelector:'.control-custom'},initialize:function(){jQuery.proxyAll(this,/_on/);var delegated=this.options.fieldSelector+':last input:first';this.el.on('change',delegated,this._onChange);this.el.on('change',':checkbox',this._onRemove);},newField:function(element){this.el.append(this.cloneField(element));},cloneField:function(current){return this.resetField(jQuery(current).clone());},resetField:function(field){function increment(index,string){return(string||'').replace(/\d+/,function(int){return 1+parseInt(int,10);});}
+var input=field.find(':input');input.val('').attr('id',increment).attr('name',increment);var label=field.find('label');label.text(increment).attr('for',increment);return field;},disableField:function(field,disable){field.toggleClass('disabled',disable!==false);},_onChange:function(event){if(event.target.value!==''){var parent=jQuery(event.target).parents(this.options.fieldSelector);this.newField(parent);}},_onRemove:function(event){var parent=jQuery(event.target).parents(this.options.fieldSelector);this.disableField(parent,event.target.checked);}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dashboard.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dashboard.js
new file mode 100644
index 00000000..965340cb
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dashboard.js
@@ -0,0 +1,86 @@
+/* User Dashboard
+ * Handles the filter dropdown menu and the reduction of the notifications number
+ * within the header to zero
+ *
+ * Examples
+ *
+ *
+ *
+ */
+this.ckan.module('dashboard', function ($) {
+ return {
+ button: null,
+ popover: null,
+ searchTimeout: null,
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ $.proxyAll(this, /_on/);
+ this.button = $('#followee-filter .btn').
+ on('click', this._onShowFolloweeDropdown);
+ var title = this.button.prop('title');
+ this.button.popover({
+ placement: 'bottom',
+ title: 'Filter',
+ html: true,
+ content: $('#followee-popover').html()
+ });
+ this.button.prop('title', title);
+ this.popover = this.button.data('bs.popover').tip().addClass('popover-followee');
+ },
+
+ /* Handles click event on the 'show me:' dropdown button
+ *
+ * Returns nothing.
+ */
+ _onShowFolloweeDropdown: function() {
+ this.button.toggleClass('active');
+ if (this.button.hasClass('active')) {
+ setTimeout(this._onInitSearch, 100);
+ }
+ return false;
+ },
+
+ /* Handles focusing on the input and making sure that the keyup
+ * even is applied to the input
+ *
+ * Returns nothing.
+ */
+ _onInitSearch: function() {
+ var input = $('input', this.popover);
+ if (!input.hasClass('inited')) {
+ input.
+ on('keyup', this._onSearchKeyUp).
+ addClass('inited');
+ }
+ input.focus();
+ },
+
+ /* Handles the keyup event
+ *
+ * Returns nothing.
+ */
+ _onSearchKeyUp: function() {
+ clearTimeout(this.searchTimeout);
+ this.searchTimeout = setTimeout(this._onSearchKeyUpTimeout, 300);
+ },
+
+ /* Handles the actual filtering of search results
+ *
+ * Returns nothing.
+ */
+ _onSearchKeyUpTimeout: function() {
+ var input = $('input', this.popover);
+ var q = input.val().toLowerCase();
+ if (q) {
+ $('li', this.popover).hide();
+ $('li.everything, [data-search^="' + q + '"]', this.popover).show();
+ } else {
+ $('li', this.popover).show();
+ }
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dashboard.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dashboard.min.js
new file mode 100644
index 00000000..502ffdb3
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dashboard.min.js
@@ -0,0 +1,3 @@
+this.ckan.module('dashboard',function($){return{button:null,popover:null,searchTimeout:null,initialize:function(){$.proxyAll(this,/_on/);this.button=$('#followee-filter .btn').on('click',this._onShowFolloweeDropdown);var title=this.button.prop('title');this.button.popover({placement:'bottom',title:'Filter',html:true,content:$('#followee-popover').html()});this.button.prop('title',title);this.popover=this.button.data('bs.popover').tip().addClass('popover-followee');},_onShowFolloweeDropdown:function(){this.button.toggleClass('active');if(this.button.hasClass('active')){setTimeout(this._onInitSearch,100);}
+return false;},_onInitSearch:function(){var input=$('input',this.popover);if(!input.hasClass('inited')){input.on('keyup',this._onSearchKeyUp).addClass('inited');}
+input.focus();},_onSearchKeyUp:function(){clearTimeout(this.searchTimeout);this.searchTimeout=setTimeout(this._onSearchKeyUpTimeout,300);},_onSearchKeyUpTimeout:function(){var input=$('input',this.popover);var q=input.val().toLowerCase();if(q){$('li',this.popover).hide();$('li.everything, [data-search^="'+q+'"]',this.popover).show();}else{$('li',this.popover).show();}}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/data-viewer.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/data-viewer.js
new file mode 100644
index 00000000..5b9cbafc
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/data-viewer.js
@@ -0,0 +1,54 @@
+// data viewer module
+// resizes the iframe when the content is loaded
+this.ckan.module('data-viewer', function (jQuery) {
+ return {
+ options: {
+ timeout: 200,
+ minHeight: 400,
+ padding: 30
+ },
+
+ initialize: function () {
+ jQuery.proxyAll(this, /_on/);
+ this.el.on('load', this._onLoad);
+ this._FirefoxFix();
+ this.sandbox.subscribe('data-viewer-error', this._onDataViewerError);
+ },
+
+ _onDataViewerError: function(message) {
+ var parent = this.el.parent();
+ $('.data-viewer-error .collapse', parent).html(message);
+ $('.data-viewer-error', parent).removeClass('js-hide');
+ this.el.hide();
+ },
+
+ _onLoad: function() {
+ var self = this;
+ var loc = $('body').data('site-root');
+ // see if page is in part of the same domain
+ if (this.el.attr('src').substring(0, loc.length) === loc) {
+ this._recalibrate();
+ setInterval(function() {
+ self._recalibrate();
+ }, this.options.timeout);
+ } else {
+ this.el.css('height', 600);
+ }
+ },
+
+ _recalibrate: function() {
+ var height = this.el.contents().find('body').outerHeight(true);
+ height = Math.max(height, this.options.minHeight);
+ this.el.css('height', height + this.options.padding);
+ },
+
+ // firefox caches iframes so force it to get fresh content
+ _FirefoxFix: function() {
+ if(/#$/.test(this.el.src)) {
+ this.el.src = this.el.src.substr(0, this.src.length - 1);
+ } else {
+ this.el.src = this.el.src + '#';
+ }
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/data-viewer.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/data-viewer.min.js
new file mode 100644
index 00000000..0ea13506
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/data-viewer.min.js
@@ -0,0 +1 @@
+this.ckan.module('data-viewer',function(jQuery){return{options:{timeout:200,minHeight:400,padding:30},initialize:function(){jQuery.proxyAll(this,/_on/);this.el.on('load',this._onLoad);this._FirefoxFix();this.sandbox.subscribe('data-viewer-error',this._onDataViewerError);},_onDataViewerError:function(message){var parent=this.el.parent();$('.data-viewer-error .collapse',parent).html(message);$('.data-viewer-error',parent).removeClass('js-hide');this.el.hide();},_onLoad:function(){var self=this;var loc=$('body').data('site-root');if(this.el.attr('src').substring(0,loc.length)===loc){this._recalibrate();setInterval(function(){self._recalibrate();},this.options.timeout);}else{this.el.css('height',600);}},_recalibrate:function(){var height=this.el.contents().find('body').outerHeight(true);height=Math.max(height,this.options.minHeight);this.el.css('height',height+this.options.padding);},_FirefoxFix:function(){if(/#$/.test(this.el.src)){this.el.src=this.el.src.substr(0,this.src.length-1);}else{this.el.src=this.el.src+'#';}}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dataset-visibility.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dataset-visibility.js
new file mode 100644
index 00000000..4d9148f2
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dataset-visibility.js
@@ -0,0 +1,32 @@
+/* Dataset visibility toggler
+ * When no organization is selected in the org dropdown then set visibility to
+ * public always and disable dropdown
+ */
+this.ckan.module('dataset-visibility', function ($) {
+ return {
+ currentValue: false,
+ options: {
+ organizations: $('#field-organizations'),
+ visibility: $('#field-private'),
+ currentValue: null
+ },
+ initialize: function() {
+ $.proxyAll(this, /_on/);
+ this.options.currentValue = this.options.visibility.val();
+ this.options.organizations.on('change', this._onOrganizationChange);
+ this._onOrganizationChange();
+ },
+ _onOrganizationChange: function() {
+ var value = this.options.organizations.val();
+ if (value) {
+ this.options.visibility
+ .prop('disabled', false)
+ .val(this.options.currentValue);
+ } else {
+ this.options.visibility
+ .prop('disabled', true)
+ .val('False');
+ }
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dataset-visibility.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dataset-visibility.min.js
new file mode 100644
index 00000000..5534893d
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/dataset-visibility.min.js
@@ -0,0 +1 @@
+this.ckan.module('dataset-visibility',function($){return{currentValue:false,options:{organizations:$('#field-organizations'),visibility:$('#field-private'),currentValue:null},initialize:function(){$.proxyAll(this,/_on/);this.options.currentValue=this.options.visibility.val();this.options.organizations.on('change',this._onOrganizationChange);this._onOrganizationChange();},_onOrganizationChange:function(){var value=this.options.organizations.val();if(value){this.options.visibility.prop('disabled',false).val(this.options.currentValue);}else{this.options.visibility.prop('disabled',true).val('False');}}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/follow.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/follow.js
new file mode 100644
index 00000000..40438f30
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/follow.js
@@ -0,0 +1,78 @@
+/* Follow buttons
+ * Handles calling the API to follow the current user
+ *
+ * action - This being the action that the button should perform. Currently: "follow" or "unfollow"
+ * type - The being the type of object the user is trying to support. Currently: "user", "group" or "dataset"
+ * id - id of the objec the user is trying to follow
+ * loading - State management helper
+ *
+ * Examples
+ *
+ * Follow User
+ *
+ */
+this.ckan.module('follow', function($) {
+ return {
+ /* options object can be extended using data-module-* attributes */
+ options : {
+ action: null,
+ type: null,
+ id: null,
+ loading: false
+ },
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ $.proxyAll(this, /_on/);
+ this.el.on('click', this._onClick);
+ },
+
+ /* Handles the clicking of the follow button
+ *
+ * event - An event object.
+ *
+ * Returns nothing.
+ */
+ _onClick: function(event) {
+ var options = this.options;
+ if (
+ options.action
+ && options.type
+ && options.id
+ && !options.loading
+ ) {
+ event.preventDefault();
+ var client = this.sandbox.client;
+ var path = options.action + '_' + options.type;
+ options.loading = true;
+ this.el.addClass('disabled');
+ client.call('POST', path, { id : options.id }, this._onClickLoaded);
+ }
+ },
+
+ /* Fired after the call to the API to either follow or unfollow
+ *
+ * json - The return json from the follow / unfollow API call
+ *
+ * Returns nothing.
+ */
+ _onClickLoaded: function(json) {
+ var options = this.options;
+ var sandbox = this.sandbox;
+ var oldAction = options.action;
+ options.loading = false;
+ this.el.removeClass('disabled');
+ if (options.action == 'follow') {
+ options.action = 'unfollow';
+ this.el.html(' ' + this._('Unfollow')).removeClass('btn-success').addClass('btn-danger');
+ } else {
+ options.action = 'follow';
+ this.el.html(' ' + this._('Follow')).removeClass('btn-danger').addClass('btn-success');
+ }
+ sandbox.publish('follow-' + oldAction + '-' + options.id);
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/follow.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/follow.min.js
new file mode 100644
index 00000000..092aa29f
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/follow.min.js
@@ -0,0 +1,2 @@
+this.ckan.module('follow',function($){return{options:{action:null,type:null,id:null,loading:false},initialize:function(){$.proxyAll(this,/_on/);this.el.on('click',this._onClick);},_onClick:function(event){var options=this.options;if(options.action&&options.type&&options.id&&!options.loading){event.preventDefault();var client=this.sandbox.client;var path=options.action+'_'+options.type;options.loading=true;this.el.addClass('disabled');client.call('POST',path,{id:options.id},this._onClickLoaded);}},_onClickLoaded:function(json){var options=this.options;var sandbox=this.sandbox;var oldAction=options.action;options.loading=false;this.el.removeClass('disabled');if(options.action=='follow'){options.action='unfollow';this.el.html(' '+this._('Unfollow')).removeClass('btn-success').addClass('btn-danger');}else{options.action='follow';this.el.html(' '+this._('Follow')).removeClass('btn-danger').addClass('btn-success');}
+sandbox.publish('follow-'+oldAction+'-'+options.id);}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/followers-counter.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/followers-counter.js
new file mode 100644
index 00000000..b6441f62
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/followers-counter.js
@@ -0,0 +1,93 @@
+/* Updates the Followers counter in the UI when the Follow/Unfollow button
+* is clicked.
+*
+* id - id of the object the user is trying to follow/unfollow.
+* num_followers - Number of followers the object has.
+*
+* Example
+*
+*
+* 6
+*
+*
+*/
+this.ckan.module('followers-counter', function($) {
+ 'use strict';
+
+ return {
+ options: {
+ id: null,
+ num_followers: 0
+ },
+
+ /* Subscribe to events when the Follow/Unfollow button is clicked.
+ *
+ * Returns nothing.
+ */
+ initialize: function() {
+ $.proxyAll(this, /_on/);
+
+ this.counterEl = this.$('span');
+ this.objId = this.options.id;
+
+ this.sandbox.subscribe('follow-follow-' + this.objId, this._onFollow);
+ this.sandbox.subscribe('follow-unfollow-' + this.objId, this._onUnfollow);
+ },
+
+ /* Calls a function to update the counter when the Follow button is clicked.
+ *
+ * Returns nothing.
+ */
+ _onFollow: function() {
+ this._updateCounter({action: 'follow'});
+ },
+
+ /* Calls a function to update the counter when the Unfollow button is clicked.
+ *
+ * Returns nothing.
+ */
+ _onUnfollow: function() {
+ this._updateCounter({action: 'unfollow'});
+ },
+
+ /* Handles updating the UI for Followers counter.
+ *
+ * Returns nothing.
+ */
+ _updateCounter: function(options) {
+ var locale = $('html').attr('lang');
+ var action = options.action;
+ var incrementedFollowers;
+
+ locale = locale ? locale.replace('_', '-') : locale;
+
+ if (action === 'follow') {
+ incrementedFollowers = (++this.options.num_followers).toLocaleString(locale);
+ } else if (action === 'unfollow') {
+ incrementedFollowers = (--this.options.num_followers).toLocaleString(locale);
+ }
+
+ // Only update the value if it's less than 1000, because for larger
+ // numbers the change won't be noticeable since the value is converted
+ // to SI number abbreviated with "k", "m" and so on.
+ if (this.options.num_followers < 1000) {
+ this.counterEl.text(incrementedFollowers);
+ this.counterEl.removeAttr('title');
+ } else {
+ this.counterEl.attr('title', incrementedFollowers);
+ }
+ },
+
+ /* Remove any subscriptions to prevent memory leaks. This function is
+ * called when a module element is removed from the page.
+ *
+ * Returns nothing.
+ */
+ teardown: function() {
+ this.sandbox.unsubscribe('follow-follow-' + this.objId, this._onFollow);
+ this.sandbox.unsubscribe('follow-unfollow-' + this.objId, this._onUnfollow);
+ }
+ }
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/followers-counter.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/followers-counter.min.js
new file mode 100644
index 00000000..31d39796
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/followers-counter.min.js
@@ -0,0 +1,2 @@
+this.ckan.module('followers-counter',function($){'use strict';return{options:{id:null,num_followers:0},initialize:function(){$.proxyAll(this,/_on/);this.counterEl=this.$('span');this.objId=this.options.id;this.sandbox.subscribe('follow-follow-'+this.objId,this._onFollow);this.sandbox.subscribe('follow-unfollow-'+this.objId,this._onUnfollow);},_onFollow:function(){this._updateCounter({action:'follow'});},_onUnfollow:function(){this._updateCounter({action:'unfollow'});},_updateCounter:function(options){var locale=$('html').attr('lang');var action=options.action;var incrementedFollowers;locale=locale?locale.replace('_','-'):locale;if(action==='follow'){incrementedFollowers=(++this.options.num_followers).toLocaleString(locale);}else if(action==='unfollow'){incrementedFollowers=(--this.options.num_followers).toLocaleString(locale);}
+if(this.options.num_followers<1000){this.counterEl.text(incrementedFollowers);this.counterEl.removeAttr('title');}else{this.counterEl.attr('title',incrementedFollowers);}},teardown:function(){this.sandbox.unsubscribe('follow-follow-'+this.objId,this._onFollow);this.sandbox.unsubscribe('follow-unfollow-'+this.objId,this._onUnfollow);}}});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/image-upload.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/image-upload.js
new file mode 100644
index 00000000..49255b71
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/image-upload.js
@@ -0,0 +1,280 @@
+ /* Image Upload
+ *
+ */
+this.ckan.module('image-upload', function($) {
+ return {
+ /* options object can be extended using data-module-* attributes */
+ options: {
+ is_url: false,
+ is_upload: false,
+ field_upload: 'image_upload',
+ field_url: 'image_url',
+ field_clear: 'clear_upload',
+ field_name: 'name',
+ upload_label: ''
+ },
+
+ /* Should be changed to true if user modifies resource's name
+ *
+ * @type {Boolean}
+ */
+ _nameIsDirty: false,
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ $.proxyAll(this, /_on/);
+ var options = this.options;
+
+ // firstly setup the fields
+ var field_upload = 'input[name="' + options.field_upload + '"]';
+ var field_url = 'input[name="' + options.field_url + '"]';
+ var field_clear = 'input[name="' + options.field_clear + '"]';
+ var field_name = 'input[name="' + options.field_name + '"]';
+
+ this.input = $(field_upload, this.el);
+ this.field_url = $(field_url, this.el).parents('.form-group');
+ this.field_image = this.input.parents('.form-group');
+ this.field_url_input = $('input', this.field_url);
+ this.field_name = this.el.parents('form').find(field_name);
+ // this is the location for the upload/link data/image label
+ this.label_location = $('label[for="field-image-url"]');
+ // determines if the resource is a data resource
+ this.is_data_resource = (this.options.field_url === 'url') && (this.options.field_upload === 'upload');
+
+ // Is there a clear checkbox on the form already?
+ var checkbox = $(field_clear, this.el);
+ if (checkbox.length > 0) {
+ checkbox.parents('.form-group').remove();
+ }
+
+ // Adds the hidden clear input to the form
+ this.field_clear = $(' ')
+ .appendTo(this.el);
+
+ // Button to set the field to be a URL
+ this.button_url = $('' +
+ ' ' +
+ this._('Link') + ' ')
+ .prop('title', this._('Link to a URL on the internet (you can also link to an API)'))
+ .on('click', this._onFromWeb)
+ .insertAfter(this.input);
+
+ // Button to attach local file to the form
+ this.button_upload = $('' +
+ ' ' +
+ this._('Upload') + ' ')
+ .insertAfter(this.input);
+
+ // Button for resetting the form when there is a URL set
+ var removeText = this._('Remove');
+ $(''
+ + removeText + ' ')
+ .prop('title', removeText)
+ .on('click', this._onRemove)
+ .insertBefore(this.field_url_input);
+
+ // Update the main label (this is displayed when no data/image has been uploaded/linked)
+ $('label[for="field-image-upload"]').text(options.upload_label || this._('Image'));
+
+ // Setup the file input
+ this.input
+ .on('mouseover', this._onInputMouseOver)
+ .on('mouseout', this._onInputMouseOut)
+ .on('change', this._onInputChange)
+ .prop('title', this._('Upload a file on your computer'))
+ .css('width', this.button_upload.outerWidth());
+
+ // Fields storage. Used in this.changeState
+ this.fields = $(' ')
+ .add(this.button_upload)
+ .add(this.button_url)
+ .add(this.input)
+ .add(this.field_url)
+ .add(this.field_image);
+
+ // Disables autoName if user modifies name field
+ this.field_name
+ .on('change', this._onModifyName);
+ // Disables autoName if resource name already has value,
+ // i.e. we on edit page
+ if (this.field_name.val()){
+ this._nameIsDirty = true;
+ }
+
+ if (options.is_url) {
+ this._showOnlyFieldUrl();
+
+ this._updateUrlLabel(this._('URL'));
+ } else if (options.is_upload) {
+ this._showOnlyFieldUrl();
+
+ this.field_url_input.prop('readonly', true);
+ // If the data is an uploaded file, the filename will display rather than whole url of the site
+ var filename = this._fileNameFromUpload(this.field_url_input.val());
+ this.field_url_input.val(filename);
+
+ this._updateUrlLabel(this._('File'));
+ } else {
+ this._showOnlyButtons();
+ }
+ },
+
+ /* Quick way of getting just the filename from the uri of the resource data
+ *
+ * url - The url of the uploaded data file
+ *
+ * Returns String.
+ */
+ _fileNameFromUpload: function(url) {
+ // If it's a local CKAN image return the entire URL.
+ if (/^\/base\/images/.test(url)) {
+ return url;
+ }
+
+ // remove fragment (#)
+ url = url.substring(0, (url.indexOf("#") === -1) ? url.length : url.indexOf("#"));
+ // remove query string
+ url = url.substring(0, (url.indexOf("?") === -1) ? url.length : url.indexOf("?"));
+ // extract the filename
+ url = url.substring(url.lastIndexOf("/") + 1, url.length);
+
+ return url; // filename
+ },
+
+ /* Update the `this.label_location` text
+ *
+ * If the upload/link is for a data resource, rather than an image,
+ * the text for label[for="field-image-url"] will be updated.
+ *
+ * label_text - The text for the label of an uploaded/linked resource
+ *
+ * Returns nothing.
+ */
+ _updateUrlLabel: function(label_text) {
+ if (! this.is_data_resource) {
+ return;
+ }
+
+ this.label_location.text(label_text);
+ },
+
+ /* Event listener for when someone sets the field to URL mode
+ *
+ * Returns nothing.
+ */
+ _onFromWeb: function() {
+ this._showOnlyFieldUrl();
+
+ this.field_url_input.focus()
+ .on('blur', this._onFromWebBlur);
+
+ if (this.options.is_upload) {
+ this.field_clear.val('true');
+ }
+
+ this._updateUrlLabel(this._('URL'));
+ },
+
+ /* Event listener for resetting the field back to the blank state
+ *
+ * Returns nothing.
+ */
+ _onRemove: function() {
+ this._showOnlyButtons();
+
+ this.field_url_input.val('');
+ this.field_url_input.prop('readonly', false);
+
+ this.field_clear.val('true');
+ },
+
+ /* Event listener for when someone chooses a file to upload
+ *
+ * Returns nothing.
+ */
+ _onInputChange: function() {
+ var file_name = this.input.val().split(/^C:\\fakepath\\/).pop();
+ this.field_url_input.val(file_name);
+ this.field_url_input.prop('readonly', true);
+
+ this.field_clear.val('');
+
+ this._showOnlyFieldUrl();
+
+ this._autoName(file_name);
+
+ this._updateUrlLabel(this._('File'));
+ },
+
+ /* Show only the buttons, hiding all others
+ *
+ * Returns nothing.
+ */
+ _showOnlyButtons: function() {
+ this.fields.hide();
+ this.button_upload
+ .add(this.field_image)
+ .add(this.button_url)
+ .add(this.input)
+ .show();
+ },
+
+ /* Show only the URL field, hiding all others
+ *
+ * Returns nothing.
+ */
+ _showOnlyFieldUrl: function() {
+ this.fields.hide();
+ this.field_url.show();
+ },
+
+ /* Event listener for when a user mouseovers the hidden file input
+ *
+ * Returns nothing.
+ */
+ _onInputMouseOver: function() {
+ this.button_upload.addClass('hover');
+ },
+
+ /* Event listener for when a user mouseouts the hidden file input
+ *
+ * Returns nothing.
+ */
+ _onInputMouseOut: function() {
+ this.button_upload.removeClass('hover');
+ },
+
+ /* Event listener for changes in resource's name by direct input from user
+ *
+ * Returns nothing
+ */
+ _onModifyName: function() {
+ this._nameIsDirty = true;
+ },
+
+ /* Event listener for when someone loses focus of URL field
+ *
+ * Returns nothing
+ */
+ _onFromWebBlur: function() {
+ var url = this.field_url_input.val().match(/([^\/]+)\/?$/)
+ if (url) {
+ this._autoName(url.pop());
+ }
+ },
+
+ /* Automatically add file name into field Name
+ *
+ * Select by attribute [name] to be on the safe side and allow to change field id
+ * Returns nothing
+ */
+ _autoName: function(name) {
+ if (!this._nameIsDirty){
+ this.field_name.val(name);
+ }
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/image-upload.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/image-upload.min.js
new file mode 100644
index 00000000..1bce1609
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/image-upload.min.js
@@ -0,0 +1,10 @@
+this.ckan.module('image-upload',function($){return{options:{is_url:false,is_upload:false,field_upload:'image_upload',field_url:'image_url',field_clear:'clear_upload',field_name:'name',upload_label:''},_nameIsDirty:false,initialize:function(){$.proxyAll(this,/_on/);var options=this.options;var field_upload='input[name="'+options.field_upload+'"]';var field_url='input[name="'+options.field_url+'"]';var field_clear='input[name="'+options.field_clear+'"]';var field_name='input[name="'+options.field_name+'"]';this.input=$(field_upload,this.el);this.field_url=$(field_url,this.el).parents('.form-group');this.field_image=this.input.parents('.form-group');this.field_url_input=$('input',this.field_url);this.field_name=this.el.parents('form').find(field_name);this.label_location=$('label[for="field-image-url"]');this.is_data_resource=(this.options.field_url==='url')&&(this.options.field_upload==='upload');var checkbox=$(field_clear,this.el);if(checkbox.length>0){checkbox.parents('.form-group').remove();}
+this.field_clear=$(' ').appendTo(this.el);this.button_url=$(''+' '+
+this._('Link')+' ').prop('title',this._('Link to a URL on the internet (you can also link to an API)')).on('click',this._onFromWeb).insertAfter(this.input);this.button_upload=$(''+' '+
+this._('Upload')+' ').insertAfter(this.input);var removeText=this._('Remove');$(''
++removeText+' ').prop('title',removeText).on('click',this._onRemove).insertBefore(this.field_url_input);$('label[for="field-image-upload"]').text(options.upload_label||this._('Image'));this.input.on('mouseover',this._onInputMouseOver).on('mouseout',this._onInputMouseOut).on('change',this._onInputChange).prop('title',this._('Upload a file on your computer')).css('width',this.button_upload.outerWidth());this.fields=$(' ').add(this.button_upload).add(this.button_url).add(this.input).add(this.field_url).add(this.field_image);this.field_name.on('change',this._onModifyName);if(this.field_name.val()){this._nameIsDirty=true;}
+if(options.is_url){this._showOnlyFieldUrl();this._updateUrlLabel(this._('URL'));}else if(options.is_upload){this._showOnlyFieldUrl();this.field_url_input.prop('readonly',true);var filename=this._fileNameFromUpload(this.field_url_input.val());this.field_url_input.val(filename);this._updateUrlLabel(this._('File'));}else{this._showOnlyButtons();}},_fileNameFromUpload:function(url){if(/^\/base\/images/.test(url)){return url;}
+url=url.substring(0,(url.indexOf("#")===-1)?url.length:url.indexOf("#"));url=url.substring(0,(url.indexOf("?")===-1)?url.length:url.indexOf("?"));url=url.substring(url.lastIndexOf("/")+1,url.length);return url;},_updateUrlLabel:function(label_text){if(!this.is_data_resource){return;}
+this.label_location.text(label_text);},_onFromWeb:function(){this._showOnlyFieldUrl();this.field_url_input.focus().on('blur',this._onFromWebBlur);if(this.options.is_upload){this.field_clear.val('true');}
+this._updateUrlLabel(this._('URL'));},_onRemove:function(){this._showOnlyButtons();this.field_url_input.val('');this.field_url_input.prop('readonly',false);this.field_clear.val('true');},_onInputChange:function(){var file_name=this.input.val().split(/^C:\\fakepath\\/).pop();this.field_url_input.val(file_name);this.field_url_input.prop('readonly',true);this.field_clear.val('');this._showOnlyFieldUrl();this._autoName(file_name);this._updateUrlLabel(this._('File'));},_showOnlyButtons:function(){this.fields.hide();this.button_upload.add(this.field_image).add(this.button_url).add(this.input).show();},_showOnlyFieldUrl:function(){this.fields.hide();this.field_url.show();},_onInputMouseOver:function(){this.button_upload.addClass('hover');},_onInputMouseOut:function(){this.button_upload.removeClass('hover');},_onModifyName:function(){this._nameIsDirty=true;},_onFromWebBlur:function(){var url=this.field_url_input.val().match(/([^\/]+)\/?$/)
+if(url){this._autoName(url.pop());}},_autoName:function(name){if(!this._nameIsDirty){this.field_name.val(name);}}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/media-grid.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/media-grid.js
new file mode 100644
index 00000000..a6e269f5
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/media-grid.js
@@ -0,0 +1,16 @@
+/* Media Grid
+ * Super simple plugin that waits for all the images to be loaded in the media
+ * grid and then applies the jQuery.masonry to then
+ */
+this.ckan.module('media-grid', function ($) {
+ return {
+ initialize: function () {
+ var wrapper = this.el;
+ wrapper.imagesLoaded(function() {
+ wrapper.masonry({
+ itemSelector: '.media-item'
+ });
+ });
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/media-grid.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/media-grid.min.js
new file mode 100644
index 00000000..2199fd43
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/media-grid.min.js
@@ -0,0 +1 @@
+this.ckan.module('media-grid',function($){return{initialize:function(){var wrapper=this.el;wrapper.imagesLoaded(function(){wrapper.masonry({itemSelector:'.media-item'});});}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/popover-context.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/popover-context.js
new file mode 100644
index 00000000..236969f0
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/popover-context.js
@@ -0,0 +1,257 @@
+/* Popover context
+ * These appear when someone hovers over a context item in a activity stream to
+ * give the user more context into that particular item. It also allows for
+ * people to follow and unfollow quickly from within the popover
+ *
+ * id - The user_id of user
+ * context - The type of this popover: currently supports user & package
+
+ * url - The URL of the profile for that user
+ * loading - Loading state helper
+ * authed - Is the current user authed ... if so what's their user_id
+ * template - Simple string-replace template for content of popover
+ *
+ * Examples
+ *
+ * A user
+ *
+ */
+
+// Global dictionary and render store for items
+window.popover_context = {
+ dict: {
+ user: {},
+ dataset: {},
+ group: {}
+ },
+ render: {
+ user: {},
+ dataset: {},
+ group: {}
+ }
+};
+
+this.ckan.module('popover-context', function($) {
+ return {
+
+ /* options object can be extended using data-module-* attributes */
+ options : {
+ id: null,
+ loading: false,
+ error: false,
+ authed: false,
+ throbber: ' '
+ },
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function() {
+ if (
+ this.options.id != true
+ && this.options.id != null
+ ) {
+ $.proxyAll(this, /_on/);
+ if ($('.account').hasClass('authed')) {
+ this.options.authed = $('.account').data('me');
+ }
+ this.el.popover({
+ animation: false,
+ html: true,
+ content: this.options.throbber.replace('{SITE_ROOT}', ckan.SITE_ROOT) + this._('Loading...'),
+ placement: 'bottom'
+ });
+ this.el.on('mouseover', this._onMouseOver);
+ $(document).on('mouseup', this._onDocumentMouseUp);
+ this.sandbox.subscribe('follow-follow-' + this.options.id, this._onHandleFollow);
+ this.sandbox.subscribe('follow-unfollow-' + this.options.id, this._onHandleFollow);
+ }
+ },
+
+ /* Get's called on document click in order to hide popover on not hit
+ *
+ * Returns nothing.
+ */
+ _onDocumentMouseUp: function(event) {
+ var popover = this.el.data('popover');
+ if (typeof popover.$tip != 'undefined') {
+ if (popover.$tip.has(event.target).length === 0) {
+ this.el.popover('hide');
+ }
+ }
+ },
+
+ /* Helper that changes the loading state of the active popover
+ *
+ * Returns nothing.
+ */
+ loadingHelper: function(loading) {
+ this.options.loading = loading;
+ var popover = this.el.data('popover');
+ if (typeof popover.$tip != 'undefined') {
+ if (loading) {
+ popover.$tip.addClass('popover-context-loading');
+ } else {
+ popover.$tip.removeClass('popover-context-loading');
+ }
+ }
+ },
+
+ /* Handles the showing of the popover on hover (also hides other active
+ * popovers)
+ *
+ * Returns nothing.
+ */
+ _onMouseOver: function() {
+ $('[data-module="popover-context"]').popover('hide');
+ this.el.popover('show');
+ this.getData();
+ },
+
+ /* Get's the data from the ckan api
+ *
+ * Returns nothing.
+ */
+ getData: function() {
+ if (!this.options.loading) {
+ this.loadingHelper(true);
+ var id = this.options.id;
+ var type = this.options.type;
+ if (typeof window.popover_context.dict[type][id] == 'undefined') {
+ var client = this.sandbox.client;
+ var endpoint = type + '_show';
+ if (type == 'dataset') {
+ endpoint = 'package_show';
+ }
+ client.call('GET', endpoint, '?id=' + id, this._onHandleData, this._onHandleError);
+ } else {
+ this._onHandleData(window.popover_context.dict[type][id]);
+ }
+ }
+ },
+
+ /* Handle's a error on the call api
+ *
+ * Returns nothing.
+ */
+ _onHandleError: function(error) {
+ $('[data-module="popover-context"][data-module-type="'+this.options.type+'"][data-module-id="'+this.options.id+'"]').popover('destroy');
+ },
+
+ /* Callback from getting the endpoint from the ckan api
+ *
+ * Returns nothing.
+ */
+ _onHandleData: function(json) {
+ if (json.success) {
+ var id = this.options.id;
+ var type = this.options.type;
+ var client = this.sandbox.client;
+ // set the dictionary
+ window.popover_context.dict[type][id] = json;
+
+ // has this been rendered before?
+ if (typeof window.popover_context.render[type][id] == 'undefined') {
+ var params = this.sanitiseParams(json.result);
+ client.getTemplate('popover_context_' + type + '.html', params, this._onRenderPopover);
+ } else {
+ this._onRenderPopover(window.popover_context.render[type][id]);
+ }
+ }
+ },
+
+ /* Used to break down a raw object into something a little more
+ * passable into a GET request
+ *
+ * Returns object.
+ */
+ sanitiseParams: function(raw) {
+ var type = this.options.type;
+ var params = {};
+ if (type == 'user') {
+ params.id = raw.id;
+ params.name = raw.name;
+ params.about = raw.about;
+ params.display_name = raw.display_name;
+ params.num_followers = raw.num_followers;
+ params.number_administered_packages = raw.number_administered_packages;
+ params.number_of_edits = raw.number_of_edits;
+ params.is_me = ( raw.id == this.options.authed );
+ } else if (type == 'dataset') {
+ params.id = raw.id;
+ params.title = raw.title;
+ params.name = raw.name;
+ params.notes = raw.notes;
+ params.num_resources = raw.num_resources;
+ params.num_tags = raw.num_tags;
+ } else if (type == 'group') {
+ params.id = raw.id;
+ params.title = raw.title;
+ params.name = raw.name;
+ params.description = raw.description;
+ params.package_count = raw.package_count;
+ params.num_followers = raw.num_followers;
+ }
+ return params;
+ },
+
+ /* Renders the contents of the popover
+ *
+ * Returns nothing.
+ */
+ _onRenderPopover: function(html) {
+ var id = this.options.id;
+ var type = this.options.type;
+ var dict = window.popover_context.dict[type][id].result;
+ var popover = this.el.data('popover');
+ if (typeof popover.$tip != 'undefined') {
+ var tip = popover.$tip;
+ var title = ( type == 'user' ) ? dict.display_name : dict.title;
+ $('.popover-title', tip).html('× ' + title);
+ $('.popover-content', tip).html(html);
+ $('.popover-close', tip).on('click', this._onClickPopoverClose);
+ var follow_check = this.getFollowButton();
+ if (follow_check) {
+ ckan.module.initializeElement(follow_check[0]);
+ }
+ this.loadingHelper(false);
+ }
+ // set the global
+ window.popover_context.render[type][id] = html;
+ },
+
+ /* Handles closing the currently open popover
+ *
+ * Returns nothing.
+ */
+ _onClickPopoverClose: function() {
+ this.el.popover('hide');
+ },
+
+ /* Handles getting the follow button form within a popover
+ *
+ * Returns jQuery collection || false.
+ */
+ getFollowButton: function() {
+ var popover = this.el.data('popover');
+ if (typeof popover.$tip != 'undefined') {
+ var button = $('[data-module="follow"]', popover.$tip);
+ if (button.length > 0) {
+ return button;
+ }
+ }
+ return false;
+ },
+
+ /* Callback from when you follow/unfollow a specified item... this is
+ * used to ensure all popovers associated to that user get re-populated
+ *
+ * Returns nothing.
+ */
+ _onHandleFollow: function() {
+ delete window.popover_context.render[this.options.type][this.options.id];
+ }
+
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/popover-context.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/popover-context.min.js
new file mode 100644
index 00000000..6ca2b3c1
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/popover-context.min.js
@@ -0,0 +1,7 @@
+window.popover_context={dict:{user:{},dataset:{},group:{}},render:{user:{},dataset:{},group:{}}};this.ckan.module('popover-context',function($){return{options:{id:null,loading:false,error:false,authed:false,throbber:' '},initialize:function(){if(this.options.id!=true&&this.options.id!=null){$.proxyAll(this,/_on/);if($('.account').hasClass('authed')){this.options.authed=$('.account').data('me');}
+this.el.popover({animation:false,html:true,content:this.options.throbber.replace('{SITE_ROOT}',ckan.SITE_ROOT)+this._('Loading...'),placement:'bottom'});this.el.on('mouseover',this._onMouseOver);$(document).on('mouseup',this._onDocumentMouseUp);this.sandbox.subscribe('follow-follow-'+this.options.id,this._onHandleFollow);this.sandbox.subscribe('follow-unfollow-'+this.options.id,this._onHandleFollow);}},_onDocumentMouseUp:function(event){var popover=this.el.data('popover');if(typeof popover.$tip!='undefined'){if(popover.$tip.has(event.target).length===0){this.el.popover('hide');}}},loadingHelper:function(loading){this.options.loading=loading;var popover=this.el.data('popover');if(typeof popover.$tip!='undefined'){if(loading){popover.$tip.addClass('popover-context-loading');}else{popover.$tip.removeClass('popover-context-loading');}}},_onMouseOver:function(){$('[data-module="popover-context"]').popover('hide');this.el.popover('show');this.getData();},getData:function(){if(!this.options.loading){this.loadingHelper(true);var id=this.options.id;var type=this.options.type;if(typeof window.popover_context.dict[type][id]=='undefined'){var client=this.sandbox.client;var endpoint=type+'_show';if(type=='dataset'){endpoint='package_show';}
+client.call('GET',endpoint,'?id='+id,this._onHandleData,this._onHandleError);}else{this._onHandleData(window.popover_context.dict[type][id]);}}},_onHandleError:function(error){$('[data-module="popover-context"][data-module-type="'+this.options.type+'"][data-module-id="'+this.options.id+'"]').popover('destroy');},_onHandleData:function(json){if(json.success){var id=this.options.id;var type=this.options.type;var client=this.sandbox.client;window.popover_context.dict[type][id]=json;if(typeof window.popover_context.render[type][id]=='undefined'){var params=this.sanitiseParams(json.result);client.getTemplate('popover_context_'+type+'.html',params,this._onRenderPopover);}else{this._onRenderPopover(window.popover_context.render[type][id]);}}},sanitiseParams:function(raw){var type=this.options.type;var params={};if(type=='user'){params.id=raw.id;params.name=raw.name;params.about=raw.about;params.display_name=raw.display_name;params.num_followers=raw.num_followers;params.number_administered_packages=raw.number_administered_packages;params.number_of_edits=raw.number_of_edits;params.is_me=(raw.id==this.options.authed);}else if(type=='dataset'){params.id=raw.id;params.title=raw.title;params.name=raw.name;params.notes=raw.notes;params.num_resources=raw.num_resources;params.num_tags=raw.num_tags;}else if(type=='group'){params.id=raw.id;params.title=raw.title;params.name=raw.name;params.description=raw.description;params.package_count=raw.package_count;params.num_followers=raw.num_followers;}
+return params;},_onRenderPopover:function(html){var id=this.options.id;var type=this.options.type;var dict=window.popover_context.dict[type][id].result;var popover=this.el.data('popover');if(typeof popover.$tip!='undefined'){var tip=popover.$tip;var title=(type=='user')?dict.display_name:dict.title;$('.popover-title',tip).html('× '+title);$('.popover-content',tip).html(html);$('.popover-close',tip).on('click',this._onClickPopoverClose);var follow_check=this.getFollowButton();if(follow_check){ckan.module.initializeElement(follow_check[0]);}
+this.loadingHelper(false);}
+window.popover_context.render[type][id]=html;},_onClickPopoverClose:function(){this.el.popover('hide');},getFollowButton:function(){var popover=this.el.data('popover');if(typeof popover.$tip!='undefined'){var button=$('[data-module="follow"]',popover.$tip);if(button.length>0){return button;}}
+return false;},_onHandleFollow:function(){delete window.popover_context.render[this.options.type][this.options.id];}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-form.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-form.js
new file mode 100644
index 00000000..f8bd136f
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-form.js
@@ -0,0 +1,55 @@
+/* Module for the resource form. Handles validation and updating the form
+ * with external data such as from a file upload.
+ */
+this.ckan.module('resource-form', function (jQuery) {
+ return {
+ /* Called by the ckan core if a corresponding element is found on the page.
+ * Handles setting up event listeners, adding elements to the page etc.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ jQuery.proxyAll(this, /_on/);
+ this.sandbox.subscribe('resource:uploaded', this._onResourceUploaded);
+ },
+
+ /* Remove any subscriptions to prevent memory leaks. This function is
+ * called when a module element is removed from the page.
+ *
+ * Returns nothing..
+ */
+ teardown: function () {
+ this.sandbox.unsubscribe('resource:uploaded', this._onResourceUploaded);
+ },
+
+ /* Callback function that loads a newly uploaded resource into the form.
+ * Handles updating the various types of form fields.
+ *
+ * resource - A resource data object.
+ *
+ * Examples
+ *
+ * this.sandbox.subscribe('resource:uploaded', this._onResourceUploaded);
+ *
+ * Returns nothing.
+ */
+ _onResourceUploaded: function (resource) {
+ var key;
+ var field;
+
+ for (key in resource) {
+ if (resource.hasOwnProperty(key)) {
+ field = this.$('[name="' + key + '"]');
+
+ if (field.is(':checkbox, :radio')) {
+ this.$('[value="' + resource[key] + '"]').prop('checked', true);
+ } else if (field.is('select')) {
+ field.prop('selected', resource[key]);
+ } else {
+ field.val(resource[key]);
+ }
+ }
+ }
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-form.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-form.min.js
new file mode 100644
index 00000000..2cdb05a7
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-form.min.js
@@ -0,0 +1 @@
+this.ckan.module('resource-form',function(jQuery){return{initialize:function(){jQuery.proxyAll(this,/_on/);this.sandbox.subscribe('resource:uploaded',this._onResourceUploaded);},teardown:function(){this.sandbox.unsubscribe('resource:uploaded',this._onResourceUploaded);},_onResourceUploaded:function(resource){var key;var field;for(key in resource){if(resource.hasOwnProperty(key)){field=this.$('[name="'+key+'"]');if(field.is(':checkbox, :radio')){this.$('[value="'+resource[key]+'"]').prop('checked',true);}else if(field.is('select')){field.prop('selected',resource[key]);}else{field.val(resource[key]);}}}}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-reorder.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-reorder.js
new file mode 100644
index 00000000..3489742a
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-reorder.js
@@ -0,0 +1,151 @@
+/* Module for reordering resources
+ */
+this.ckan.module('resource-reorder', function($) {
+ return {
+ options: {
+ id: false
+ },
+ template: {
+ title: ' ',
+ help_text: '
',
+ button: [
+ '',
+ ' ',
+ ' ',
+ ' '
+ ].join('\n'),
+ form_actions: [
+ ''
+ ].join('\n'),
+ handle: [
+ '',
+ ' ',
+ ' '
+ ].join('\n'),
+ saving: [
+ '',
+ ' ',
+ ' ',
+ ' '
+ ].join('\n')
+ },
+ is_reordering: false,
+ cache: false,
+
+ initialize: function() {
+ jQuery.proxyAll(this, /_on/);
+
+ var labelText = this._('Reorder resources');
+ var helpText = this._('You can rearrange the resources by dragging them using the arrow icon. Drag the resource ' +
+ 'to the right and place it to the desired location on the list. When you are done, click the "Save order" -button.');
+
+ this.html_title = $(this.template.title)
+ .text(labelText)
+ .insertBefore(this.el)
+ .hide();
+
+ this.html_help_text = $(this.template.help_text)
+ .text(helpText)
+ .insertBefore(this.el)
+ .hide();
+
+ var button = $(this.template.button)
+ .on('click', this._onHandleStartReorder)
+ .appendTo('.page_primary_action');
+ $('span', button).text(labelText);
+
+ this.html_form_actions = $(this.template.form_actions)
+ .hide()
+ .insertAfter(this.el);
+ $('.save', this.html_form_actions)
+ .text(this._('Save order'))
+ .on('click', this._onHandleSave);
+ $('.cancel', this.html_form_actions)
+ .text(this._('Cancel'))
+ .on('click', this._onHandleCancel);
+
+ this.html_handles = $(this.template.handle)
+ .hide()
+ .appendTo($('.resource-item', this.el));
+
+ this.html_saving = $(this.template.saving)
+ .hide()
+ .insertBefore($('.save', this.html_form_actions));
+ $('span', this.html_saving).text(this._('Saving...'));
+
+ this.cache = this.el.html();
+
+ this.el
+ .sortable()
+ .sortable('disable');
+
+ },
+
+ _onHandleStartReorder: function() {
+ if (!this.is_reordering) {
+ this.html_form_actions
+ .add(this.html_handles)
+ .add(this.html_title)
+ .add(this.html_help_text)
+ .show();
+ this.el
+ .addClass('reordering')
+ .sortable('enable');
+ $('.page_primary_action').hide();
+ this.is_reordering = true;
+ }
+ },
+
+ _onHandleCancel: function() {
+ if (
+ this.is_reordering
+ && !$('.cancel', this.html_form_actions).hasClass('disabled')
+ ) {
+ this.reset();
+ this.is_reordering = false;
+ this.el.html(this.cache)
+ .sortable()
+ .sortable('disable');
+ this.html_handles = $('.handle', this.el);
+ }
+ },
+
+ _onHandleSave: function() {
+ if (!$('.save', this.html_form_actions).hasClass('disabled')) {
+ var module = this;
+ module.html_saving.show();
+ $('.save, .cancel', module.html_form_actions).addClass('disabled');
+ var order = [];
+ $('.resource-item', module.el).each(function() {
+ order.push($(this).data('id'));
+ });
+ module.sandbox.client.call('POST', 'package_resource_reorder', {
+ id: module.options.id,
+ order: order
+ }, function() {
+ module.html_saving.hide();
+ $('.save, .cancel', module.html_form_actions).removeClass('disabled');
+ module.cache = module.el.html();
+ module.reset();
+ module.is_reordering = false;
+ });
+ }
+ },
+
+ reset: function() {
+ this.html_form_actions
+ .add(this.html_handles)
+ .add(this.html_title)
+ .add(this.html_help_text)
+ .hide();
+ this.el
+ .removeClass('reordering')
+ .sortable('disable');
+ $('.page_primary_action').show();
+ }
+
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-reorder.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-reorder.min.js
new file mode 100644
index 00000000..21570394
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-reorder.min.js
@@ -0,0 +1 @@
+this.ckan.module('resource-reorder',function($){return{options:{id:false},template:{title:' ',help_text:'
',button:['',' ',' ',' '].join('\n'),form_actions:[''].join('\n'),handle:['',' ',' '].join('\n'),saving:['',' ',' ',' '].join('\n')},is_reordering:false,cache:false,initialize:function(){jQuery.proxyAll(this,/_on/);var labelText=this._('Reorder resources');var helpText=this._('You can rearrange the resources by dragging them using the arrow icon. Drag the resource '+'to the right and place it to the desired location on the list. When you are done, click the "Save order" -button.');this.html_title=$(this.template.title).text(labelText).insertBefore(this.el).hide();this.html_help_text=$(this.template.help_text).text(helpText).insertBefore(this.el).hide();var button=$(this.template.button).on('click',this._onHandleStartReorder).appendTo('.page_primary_action');$('span',button).text(labelText);this.html_form_actions=$(this.template.form_actions).hide().insertAfter(this.el);$('.save',this.html_form_actions).text(this._('Save order')).on('click',this._onHandleSave);$('.cancel',this.html_form_actions).text(this._('Cancel')).on('click',this._onHandleCancel);this.html_handles=$(this.template.handle).hide().appendTo($('.resource-item',this.el));this.html_saving=$(this.template.saving).hide().insertBefore($('.save',this.html_form_actions));$('span',this.html_saving).text(this._('Saving...'));this.cache=this.el.html();this.el.sortable().sortable('disable');},_onHandleStartReorder:function(){if(!this.is_reordering){this.html_form_actions.add(this.html_handles).add(this.html_title).add(this.html_help_text).show();this.el.addClass('reordering').sortable('enable');$('.page_primary_action').hide();this.is_reordering=true;}},_onHandleCancel:function(){if(this.is_reordering&&!$('.cancel',this.html_form_actions).hasClass('disabled')){this.reset();this.is_reordering=false;this.el.html(this.cache).sortable().sortable('disable');this.html_handles=$('.handle',this.el);}},_onHandleSave:function(){if(!$('.save',this.html_form_actions).hasClass('disabled')){var module=this;module.html_saving.show();$('.save, .cancel',module.html_form_actions).addClass('disabled');var order=[];$('.resource-item',module.el).each(function(){order.push($(this).data('id'));});module.sandbox.client.call('POST','package_resource_reorder',{id:module.options.id,order:order},function(){module.html_saving.hide();$('.save, .cancel',module.html_form_actions).removeClass('disabled');module.cache=module.el.html();module.reset();module.is_reordering=false;});}},reset:function(){this.html_form_actions.add(this.html_handles).add(this.html_title).add(this.html_help_text).hide();this.el.removeClass('reordering').sortable('disable');$('.page_primary_action').show();}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-upload-field.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-upload-field.js
new file mode 100644
index 00000000..354491f8
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-upload-field.js
@@ -0,0 +1,283 @@
+/* This module creates a new resource_type field for an uploaded file and
+ * appends file input into the page.
+ *
+ * Events:
+ *
+ * Publishes the 'resource:uploaded' event when a file is successfully
+ * uploaded. An callbacks receive an object of resource data.
+ *
+ * See: http://docs.ckan.org/en/latest/filestore.html
+ *
+ * options - form: General form overrides for the upload.
+ * template: Optional template can be provided.
+ *
+ */
+this.ckan.module('resource-upload-field', function (jQuery) {
+ return {
+ /* Default options for the module */
+ options: {
+ form: {
+ method: 'POST',
+ file: 'file',
+ params: []
+ },
+ template: [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' '
+ ].join('\n')
+ },
+
+ /* Initializes the module, creates new elements and registers event
+ * listeners etc. This method is called by ckan.initialize() if there
+ * is a corresponding element on the page.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ jQuery.proxyAll(this, /_on/);
+
+ this.upload = jQuery(this.options.template);
+ this.setupFileUpload();
+ this.el.append(this.upload);
+
+ jQuery(window).on('beforeunload', this._onWindowUpload);
+ },
+
+ /* Sets up the jQuery.fileUpload() plugin with the provided options.
+ *
+ * Returns nothing.
+ */
+ setupFileUpload: function () {
+ var options = this.options;
+
+ this.upload.find('label').text(this._('Upload a file'));
+ this.upload.find('input[type=file]').fileupload({
+ type: options.form.method,
+ paramName: options.form.file,
+ forceIframeTransport: true, // Required for XDomain request.
+ replaceFileInput: true,
+ autoUpload: false,
+ add: this._onUploadAdd,
+ send: this._onUploadSend,
+ done: this._onUploadDone,
+ fail: this._onUploadFail,
+ always: this._onUploadComplete
+ });
+ },
+
+ /* Displays a loading spinner next to the input while uploading. This
+ * can be cancelled by recalling the method passing false as the first
+ * argument.
+ *
+ * show - If false hides the spinner (default: true).
+ *
+ * Examples
+ *
+ * module.loading(); // Show spinner
+ *
+ * module.loading(false); // Hide spinner.
+ *
+ * Returns nothing.
+ */
+ loading: function (show) {
+ this.upload.toggleClass('loading', show);
+ },
+
+ /* Requests Authentication for the upload from CKAN. Uses the
+ * _onAuthSuccess/_onAuthError callbacks.
+ *
+ * key - A unique key for the file that is to be uploaded.
+ * data - The file data object from the jQuery.fileUpload() plugin.
+ *
+ * Examples
+ *
+ * onFileAdd: function (event, data) {
+ * this.authenticate('my-file', data);
+ * }
+ *
+ * Returns an jqXHR promise.
+ */
+ authenticate: function (key, data) {
+ data.key = key;
+
+ var request = this.sandbox.client.getStorageAuth(key);
+ var onSuccess = jQuery.proxy(this._onAuthSuccess, this, data);
+ return request.then(onSuccess, this._onAuthError);
+ },
+
+ /* Requests file metadata for the uploaded file and calls the
+ * _onMetadataSuccess/_onMetadataError callbacks.
+ *
+ * key - A unique key for the file that is to be uploaded.
+ * data - The file data object from the jQuery.fileUpload() plugin.
+ *
+ * Examples
+ *
+ * onFileUploaded: function (event, data) {
+ * this.lookupMetadata('my-file', data);
+ * }
+ *
+ * Returns an jqXHR promise.
+ */
+ lookupMetadata: function (key, data) {
+ var request = this.sandbox.client.getStorageMetadata(key);
+ var onSuccess = jQuery.proxy(this._onMetadataSuccess, this, data);
+ return request.then(onSuccess, this._onMetadataError);
+ },
+
+ /* Displays a global notification for the upload status.
+ *
+ * message - A message string to display.
+ * type - The type of message eg. error/info/warning
+ *
+ * Examples
+ *
+ * module.notify('Upload failed', 'error');
+ *
+ * Returns nothing.
+ */
+ notify: function (message, type) {
+ var title = this._('An Error Occurred');
+ this.sandbox.notify(title, message, type);
+ },
+
+ /* Creates a unique key for the filename provided. This is a url
+ * safe string with a timestamp prepended.
+ *
+ * filename - The filename for the upload.
+ *
+ * Examples
+ *
+ * module.generateKey('my file');
+ * // => '2012-06-05T12:00:00.000Z/my-file'
+ *
+ * Returns a unique string.
+ */
+ generateKey: function (filename) {
+ var parts = filename.split('.');
+ var extension = jQuery.url.slugify(parts.pop());
+
+ // Clean up the filename hopefully leaving the extension intact.
+ filename = jQuery.url.slugify(parts.join('.')) + '.' + extension;
+ return jQuery.date.toISOString() + '/' + filename;
+ },
+
+ /* Attaches the beforeunload event to window to prevent away navigation
+ * whilst a upload is happening
+ *
+ * is_uploading: Boolean of whether we're uploading right now
+ *
+ * Returns nothing
+ */
+ uploading: function(is_uploading) {
+ var method = is_uploading ? 'on' : 'off';
+ jQuery(window)[method]('beforeunload', this._onWindowBeforeUnload);
+ },
+
+ /* Callback called when the jQuery file upload plugin receives a file.
+ *
+ * event - The jQuery event object.
+ * data - An object of file data.
+ *
+ * Returns nothing.
+ */
+ _onUploadAdd: function (event, data) {
+ this.uploading(true);
+ if (data.files && data.files.length) {
+ for (var i = 0; i < data.files.length; i++) {
+ data.files[i].name = data.files[i].name.split('/').pop();
+ }
+ var key = this.generateKey(data.files[0].name);
+
+ this.authenticate(key, data);
+ }
+ },
+
+ /* Callback called when the jQuery file upload plugin fails to upload
+ * a file.
+ */
+ _onUploadFail: function () {
+ this.sandbox.notify(this._('Unable to upload file'));
+ },
+
+ /* Callback called when jQuery file upload plugin sends a file */
+ _onUploadSend: function () {
+ this.loading();
+ },
+
+ /* Callback called when jQuery file upload plugin successfully uploads a file */
+ _onUploadDone: function (event, data) {
+ // Need to check for a result key. A Google upload can return a 404 if
+ // the bucket does not exist, this is still treated as a success by the
+ // form upload plugin.
+ var result = data.result;
+ if (result && !(jQuery.isPlainObject(result) && result.error)) {
+ this.lookupMetadata(data.key, data);
+ } else {
+ this._onUploadFail(event, data);
+ }
+ },
+
+ /* Callback called when jQuery file upload plugin completes a request
+ * regardless of it's success/failure.
+ */
+ _onUploadComplete: function () {
+ this.loading(false);
+ this.uploading(false);
+ },
+
+ /* Callback function for a successful Auth request. This cannot be
+ * used straight up but requires the data object to be passed in
+ * as the first argument.
+ *
+ * data - The data object for the current upload.
+ * response - The auth response object.
+ *
+ * Examples
+ *
+ * var onSuccess = jQuery.proxy(this._onAuthSuccess, this, data);
+ * sandbox.client.getStorageAuth(key).done(onSuccess);
+ *
+ * Returns nothing.
+ */
+ _onAuthSuccess: function (data, response) {
+ data.url = response.action;
+ data.formData = this.options.form.params.concat(response.fields);
+ data.submit();
+ },
+
+ /* Called when the request for auth credentials fails. */
+ _onAuthError: function (event, data) {
+ this.sandbox.notify(this._('Unable to authenticate upload'));
+ this._onUploadComplete();
+ },
+
+ /* Called when the request for file metadata succeeds */
+ _onMetadataSuccess: function (data, response) {
+ var resource = this.sandbox.client.convertStorageMetadataToResource(response);
+
+ this.sandbox.notify(this._('Resource uploaded'), '', 'success');
+ this.sandbox.publish('resource:uploaded', resource);
+ },
+
+ /* Called when the request for file metadata fails */
+ _onMetadataError: function () {
+ this.sandbox.notify(this._('Unable to get data for uploaded file'));
+ this._onUploadComplete();
+ },
+
+ /* Called before the window unloads whilst uploading */
+ _onWindowBeforeUnload: function(event) {
+ var message = this._('You are uploading a file. Are you sure you ' +
+ 'want to navigate away and stop this upload?');
+ if (event.originalEvent.returnValue) {
+ event.originalEvent.returnValue = message;
+ }
+ return message;
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-upload-field.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-upload-field.min.js
new file mode 100644
index 00000000..1d6c5b33
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-upload-field.min.js
@@ -0,0 +1,3 @@
+this.ckan.module('resource-upload-field',function(jQuery){return{options:{form:{method:'POST',file:'file',params:[]},template:['',' ',' ',' ',' ',' '].join('\n')},initialize:function(){jQuery.proxyAll(this,/_on/);this.upload=jQuery(this.options.template);this.setupFileUpload();this.el.append(this.upload);jQuery(window).on('beforeunload',this._onWindowUpload);},setupFileUpload:function(){var options=this.options;this.upload.find('label').text(this._('Upload a file'));this.upload.find('input[type=file]').fileupload({type:options.form.method,paramName:options.form.file,forceIframeTransport:true,replaceFileInput:true,autoUpload:false,add:this._onUploadAdd,send:this._onUploadSend,done:this._onUploadDone,fail:this._onUploadFail,always:this._onUploadComplete});},loading:function(show){this.upload.toggleClass('loading',show);},authenticate:function(key,data){data.key=key;var request=this.sandbox.client.getStorageAuth(key);var onSuccess=jQuery.proxy(this._onAuthSuccess,this,data);return request.then(onSuccess,this._onAuthError);},lookupMetadata:function(key,data){var request=this.sandbox.client.getStorageMetadata(key);var onSuccess=jQuery.proxy(this._onMetadataSuccess,this,data);return request.then(onSuccess,this._onMetadataError);},notify:function(message,type){var title=this._('An Error Occurred');this.sandbox.notify(title,message,type);},generateKey:function(filename){var parts=filename.split('.');var extension=jQuery.url.slugify(parts.pop());filename=jQuery.url.slugify(parts.join('.'))+'.'+extension;return jQuery.date.toISOString()+'/'+filename;},uploading:function(is_uploading){var method=is_uploading?'on':'off';jQuery(window)[method]('beforeunload',this._onWindowBeforeUnload);},_onUploadAdd:function(event,data){this.uploading(true);if(data.files&&data.files.length){for(var i=0;i';
+ }
+
+ return {
+ initialize: initialize,
+ options: {
+ id: 0,
+ url: '#',
+ width: 700,
+ height: 400
+ }
+ }
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-embed.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-embed.min.js
new file mode 100644
index 00000000..4a2ebca3
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-embed.min.js
@@ -0,0 +1,9 @@
+this.ckan.module('resource-view-embed',function($){var modal;var self;function initialize(){self=this;modal=$('#embed-'+this.options.id)
+$('body').append(modal);this.el.on('click',_onClick);$('textarea',modal).on('focus',_selectAllCode).on('mouseup',_preventClick);$('input',modal).on('keyup change',_updateValues);_updateEmbedCode();}
+function _onClick(event){event.preventDefault();modal.modal('show');}
+function _selectAllCode(){$('textarea',modal).select();}
+function _updateValues(){self.options.width=$('[name="width"]',modal).val();self.options.height=$('[name="height"]',modal).val();_updateEmbedCode();}
+function _updateEmbedCode(){$('[name="code"]',modal).val(_embedCode());}
+function _preventClick(event){event.preventDefault();}
+function _embedCode(){return'';}
+return{initialize:initialize,options:{id:0,url:'#',width:700,height:400}}});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters-form.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters-form.js
new file mode 100644
index 00000000..8dd976ce
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters-form.js
@@ -0,0 +1,104 @@
+ckan.module('resource-view-filters-form', function (jQuery) {
+ 'use strict';
+
+ function applyDropdown(selectField, resourceId) {
+ var inputField = selectField.parent().find('input'),
+ filterName = selectField.val(),
+ queryLimit = 20;
+
+ inputField.select2({
+ width: 'resolve',
+ minimumInputLength: 0,
+ ajax: {
+ url: ckan.url('/api/3/action/datastore_search'),
+ datatype: 'json',
+ quietMillis: 200,
+ cache: true,
+ data: function (term, page) {
+ var offset = (page - 1) * queryLimit,
+ query;
+
+ query = {
+ resource_id: resourceId,
+ limit: queryLimit,
+ offset: offset,
+ fields: filterName,
+ distinct: true,
+ sort: filterName,
+ include_total: false
+ };
+
+ if (term !== '') {
+ var q = {};
+ if (term.indexOf(' ') == -1) {
+ term = term + ':*';
+ query.plain = false;
+ }
+ q[filterName] = term;
+ query.q = JSON.stringify(q);
+ }
+
+ return query;
+ },
+ results: function (data, page) {
+ var records = data.result.records,
+ hasMore = (records.length == queryLimit),
+ results;
+
+ results = $.map(records, function (record) {
+ return { id: record[filterName], text: String(record[filterName]) };
+ });
+
+ return { results: results, more: hasMore };
+ },
+ },
+ initSelection: function (element, callback) {
+ var data = {id: element.val(), text: element.val()};
+ callback(data);
+ },
+ });
+ }
+
+ function initialize() {
+ var self = this,
+ resourceId = self.options.resourceId,
+ templateFilterInputs = self.options.templateFilterInputs,
+ inputFieldTemplateEl = $(templateFilterInputs).find('input[type="text"][name]'),
+ filtersDiv = self.el.find(self.options.filtersSelector),
+ addFilterEl = self.el.find(self.options.addFilterSelector),
+ removeFilterSelector = self.options.removeFilterSelector;
+
+ var selects = filtersDiv.find('select');
+ selects.each(function (i, select) {
+ applyDropdown($(select), resourceId);
+ });
+
+ addFilterEl.click(function (evt) {
+ var selectField;
+ evt.preventDefault();
+ filtersDiv.append(templateFilterInputs);
+ selectField = filtersDiv.children().last().find('select');
+ applyDropdown(selectField, resourceId);
+ });
+
+ filtersDiv.on('click', removeFilterSelector, function (evt) {
+ evt.preventDefault();
+ $(this).parent().remove();
+ });
+
+ filtersDiv.on('change', 'select', function (evt) {
+ var el = $(this),
+ parentEl = el.parent(),
+ inputField = parentEl.find('input'),
+ select2Container = parentEl.find('.select2-container');
+ evt.preventDefault();
+ select2Container.remove();
+ inputField.replaceWith(inputFieldTemplateEl.clone());
+ applyDropdown(el, resourceId);
+ });
+ }
+
+ return {
+ initialize: initialize
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters-form.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters-form.min.js
new file mode 100644
index 00000000..7f9dadb7
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters-form.min.js
@@ -0,0 +1,5 @@
+ckan.module('resource-view-filters-form',function(jQuery){'use strict';function applyDropdown(selectField,resourceId){var inputField=selectField.parent().find('input'),filterName=selectField.val(),queryLimit=20;inputField.select2({width:'resolve',minimumInputLength:0,ajax:{url:ckan.url('/api/3/action/datastore_search'),datatype:'json',quietMillis:200,cache:true,data:function(term,page){var offset=(page-1)*queryLimit,query;query={resource_id:resourceId,limit:queryLimit,offset:offset,fields:filterName,distinct:true,sort:filterName,include_total:false};if(term!==''){var q={};if(term.indexOf(' ')==-1){term=term+':*';query.plain=false;}
+q[filterName]=term;query.q=JSON.stringify(q);}
+return query;},results:function(data,page){var records=data.result.records,hasMore=(records.length==queryLimit),results;results=$.map(records,function(record){return{id:record[filterName],text:String(record[filterName])};});return{results:results,more:hasMore};},},initSelection:function(element,callback){var data={id:element.val(),text:element.val()};callback(data);},});}
+function initialize(){var self=this,resourceId=self.options.resourceId,templateFilterInputs=self.options.templateFilterInputs,inputFieldTemplateEl=$(templateFilterInputs).find('input[type="text"][name]'),filtersDiv=self.el.find(self.options.filtersSelector),addFilterEl=self.el.find(self.options.addFilterSelector),removeFilterSelector=self.options.removeFilterSelector;var selects=filtersDiv.find('select');selects.each(function(i,select){applyDropdown($(select),resourceId);});addFilterEl.click(function(evt){var selectField;evt.preventDefault();filtersDiv.append(templateFilterInputs);selectField=filtersDiv.children().last().find('select');applyDropdown(selectField,resourceId);});filtersDiv.on('click',removeFilterSelector,function(evt){evt.preventDefault();$(this).parent().remove();});filtersDiv.on('change','select',function(evt){var el=$(this),parentEl=el.parent(),inputField=parentEl.find('input'),select2Container=parentEl.find('.select2-container');evt.preventDefault();select2Container.remove();inputField.replaceWith(inputFieldTemplateEl.clone());applyDropdown(el,resourceId);});}
+return{initialize:initialize};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters.js
new file mode 100644
index 00000000..5a9ffaec
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters.js
@@ -0,0 +1,186 @@
+this.ckan.module('resource-view-filters', function (jQuery) {
+ 'use strict';
+
+ function initialize() {
+ var self = this,
+ resourceId = self.options.resourceId,
+ fields = self.options.fields,
+ dropdownTemplate = self.options.dropdownTemplate,
+ addFilterTemplate = '' + self._('Add Filter') + ' ',
+ filtersDiv = $('
');
+
+ var filters = ckan.views.filters.get();
+ _appendDropdowns(filtersDiv, resourceId, dropdownTemplate, fields, filters);
+ var addFilterButton = _buildAddFilterButton(self, filtersDiv, addFilterTemplate,
+ fields, filters, function (evt) {
+ // Build filters object with this element's val as key and a placeholder
+ // value so _appendDropdowns() will create its dropdown
+ var filters = {};
+ filters[evt.val] = [];
+
+ $(this).select2('destroy');
+ _appendDropdowns(filtersDiv, resourceId, dropdownTemplate, fields, filters);
+ evt.preventDefault();
+ });
+ self.el.append(filtersDiv);
+ self.el.append(addFilterButton);
+ }
+
+ function _buildAddFilterButton(self, el, template, fields, filters, onChangeCallback) {
+ var addFilterButton = $(template),
+ currentFilters = Object.keys(filters),
+ fieldsNotFiltered = $.grep(fields, function (field) {
+ return !filters.hasOwnProperty(field);
+ }),
+ data = $.map(fieldsNotFiltered, function (d) {
+ return { id: d, text: d };
+ });
+
+ if (data.length === 0) {
+ return '';
+ }
+
+ addFilterButton.click(function (evt) {
+ // FIXME: Move this class name to some external variable to keep it DRY
+ var addFilterDiv = $('
'),
+ addFilterInput = addFilterDiv.find('input');
+ el.append(addFilterDiv);
+
+ // TODO: Remove element from "data" when some select selects it.
+ addFilterInput.select2({
+ data: data,
+ placeholder: self._('Select a field'),
+ width: 'resolve',
+ }).on('change', onChangeCallback);
+
+ evt.preventDefault();
+ });
+
+ return addFilterButton;
+ }
+
+ function _appendDropdowns(dropdowns, resourceId, template, fields, filters) {
+ $.each(fields, function (i, field) {
+ if (filters.hasOwnProperty(field)) {
+ dropdowns.append(_buildDropdown(self.el, template, field));
+ }
+ });
+
+ return dropdowns;
+
+ function _buildDropdown(el, template, filterName) {
+ var theseFilters = filters[filterName] || [];
+ template = $(template.replace(/{filter}/g, filterName));
+ // FIXME: Get the CSS class from some external variable
+ var dropdowns = template.find('.resource-view-filter-values');
+
+ // Can't use push because we need to create a new array, as we're
+ // modifying it.
+ theseFilters = theseFilters.concat([undefined]);
+ theseFilters.forEach(function (value, i) {
+ var dropdown = $(' ');
+
+ if (value !== undefined) {
+ dropdown.val(value);
+ }
+
+ dropdowns.append(dropdown);
+ });
+
+ var queryLimit = 20;
+ dropdowns.find('input').select2({
+ allowClear: true,
+ placeholder: ' ', // select2 needs a placeholder to allow clearing
+ width: 'resolve',
+ minimumInputLength: 0,
+ ajax: {
+ url: ckan.url('/api/3/action/datastore_search'),
+ datatype: 'json',
+ quietMillis: 200,
+ cache: true,
+ data: function (term, page) {
+ var offset = (page - 1) * queryLimit,
+ query;
+
+ query = {
+ resource_id: resourceId,
+ limit: queryLimit,
+ offset: offset,
+ fields: filterName,
+ distinct: true,
+ sort: filterName,
+ include_total: false
+ };
+
+ if (term !== '') {
+ var q = {};
+ if (term.indexOf(' ') == -1) {
+ term = term + ':*';
+ query.plain = false;
+ }
+ q[filterName] = term;
+ query.q = JSON.stringify(q);
+ }
+
+ return query;
+ },
+ results: function (data, page) {
+ var records = data.result.records,
+ hasMore = (records.length == queryLimit),
+ results;
+
+ results = $.map(records, function (record) {
+ return { id: record[filterName], text: String(record[filterName]) };
+ });
+
+ return { results: results, more: hasMore };
+ }
+ },
+ initSelection: function (element, callback) {
+ var data = {id: element.val(), text: element.val()};
+ callback(data);
+ },
+ }).on('change', _onChange);
+
+ return template;
+ }
+ }
+
+ function _onChange(evt) {
+ var filterName = evt.currentTarget.name,
+ filterValue = evt.val,
+ currentFilters = ckan.views.filters.get(filterName) || [],
+ addToIndex = currentFilters.length;
+
+ // Make sure we're not editing the original array, but a copy.
+ currentFilters = currentFilters.slice();
+
+ if (evt.removed) {
+ addToIndex = currentFilters.indexOf(evt.removed.id);
+ if (addToIndex !== -1) {
+ currentFilters.splice(addToIndex, 1);
+ }
+ }
+ if (evt.added) {
+ currentFilters.splice(addToIndex, 0, filterValue);
+ }
+
+ if (currentFilters.length > 0) {
+ ckan.views.filters.set(filterName, currentFilters);
+ } else {
+ ckan.views.filters.unset(filterName);
+ }
+ }
+
+ return {
+ initialize: initialize,
+ options: {
+ dropdownTemplate: [
+ '',
+ ' {filter}:',
+ '
',
+ '
',
+ ].join('\n')
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters.min.js
new file mode 100644
index 00000000..9938d529
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-filters.min.js
@@ -0,0 +1,11 @@
+this.ckan.module('resource-view-filters',function(jQuery){'use strict';function initialize(){var self=this,resourceId=self.options.resourceId,fields=self.options.fields,dropdownTemplate=self.options.dropdownTemplate,addFilterTemplate=''+self._('Add Filter')+' ',filtersDiv=$('
');var filters=ckan.views.filters.get();_appendDropdowns(filtersDiv,resourceId,dropdownTemplate,fields,filters);var addFilterButton=_buildAddFilterButton(self,filtersDiv,addFilterTemplate,fields,filters,function(evt){var filters={};filters[evt.val]=[];$(this).select2('destroy');_appendDropdowns(filtersDiv,resourceId,dropdownTemplate,fields,filters);evt.preventDefault();});self.el.append(filtersDiv);self.el.append(addFilterButton);}
+function _buildAddFilterButton(self,el,template,fields,filters,onChangeCallback){var addFilterButton=$(template),currentFilters=Object.keys(filters),fieldsNotFiltered=$.grep(fields,function(field){return!filters.hasOwnProperty(field);}),data=$.map(fieldsNotFiltered,function(d){return{id:d,text:d};});if(data.length===0){return'';}
+addFilterButton.click(function(evt){var addFilterDiv=$('
'),addFilterInput=addFilterDiv.find('input');el.append(addFilterDiv);addFilterInput.select2({data:data,placeholder:self._('Select a field'),width:'resolve',}).on('change',onChangeCallback);evt.preventDefault();});return addFilterButton;}
+function _appendDropdowns(dropdowns,resourceId,template,fields,filters){$.each(fields,function(i,field){if(filters.hasOwnProperty(field)){dropdowns.append(_buildDropdown(self.el,template,field));}});return dropdowns;function _buildDropdown(el,template,filterName){var theseFilters=filters[filterName]||[];template=$(template.replace(/{filter}/g,filterName));var dropdowns=template.find('.resource-view-filter-values');theseFilters=theseFilters.concat([undefined]);theseFilters.forEach(function(value,i){var dropdown=$(' ');if(value!==undefined){dropdown.val(value);}
+dropdowns.append(dropdown);});var queryLimit=20;dropdowns.find('input').select2({allowClear:true,placeholder:' ',width:'resolve',minimumInputLength:0,ajax:{url:ckan.url('/api/3/action/datastore_search'),datatype:'json',quietMillis:200,cache:true,data:function(term,page){var offset=(page-1)*queryLimit,query;query={resource_id:resourceId,limit:queryLimit,offset:offset,fields:filterName,distinct:true,sort:filterName,include_total:false};if(term!==''){var q={};if(term.indexOf(' ')==-1){term=term+':*';query.plain=false;}
+q[filterName]=term;query.q=JSON.stringify(q);}
+return query;},results:function(data,page){var records=data.result.records,hasMore=(records.length==queryLimit),results;results=$.map(records,function(record){return{id:record[filterName],text:String(record[filterName])};});return{results:results,more:hasMore};}},initSelection:function(element,callback){var data={id:element.val(),text:element.val()};callback(data);},}).on('change',_onChange);return template;}}
+function _onChange(evt){var filterName=evt.currentTarget.name,filterValue=evt.val,currentFilters=ckan.views.filters.get(filterName)||[],addToIndex=currentFilters.length;currentFilters=currentFilters.slice();if(evt.removed){addToIndex=currentFilters.indexOf(evt.removed.id);if(addToIndex!==-1){currentFilters.splice(addToIndex,1);}}
+if(evt.added){currentFilters.splice(addToIndex,0,filterValue);}
+if(currentFilters.length>0){ckan.views.filters.set(filterName,currentFilters);}else{ckan.views.filters.unset(filterName);}}
+return{initialize:initialize,options:{dropdownTemplate:['',].join('\n')}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-reorder.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-reorder.js
new file mode 100644
index 00000000..b4e75cb9
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-reorder.js
@@ -0,0 +1,140 @@
+/* Module for reordering resource views
+ */
+this.ckan.module('resource-view-reorder', function($) {
+ return {
+ options: {
+ id: false,
+ labelText: 'Reorder resource view'
+ },
+ template: {
+ title: ' ',
+ button: [
+ '',
+ ' ',
+ ' ',
+ ' '
+ ].join('\n'),
+ form_actions: [
+ ''
+ ].join('\n'),
+ handle: [
+ '',
+ ' ',
+ ' '
+ ].join('\n'),
+ saving: [
+ '',
+ ' ',
+ ' ',
+ ' '
+ ].join('\n')
+ },
+ is_reordering: false,
+ cache: false,
+
+ initialize: function() {
+ jQuery.proxyAll(this, /_on/);
+
+ var labelText = this._(this.options.labelText);
+ this.html_title = $(this.template.title)
+ .text(labelText)
+ .insertBefore(this.el)
+ .hide();
+ var button = $(this.template.button)
+ .on('click', this._onHandleStartReorder)
+ .appendTo('.page_primary_action');
+ $('span', button).text(labelText);
+
+ this.html_form_actions = $(this.template.form_actions)
+ .hide()
+ .insertAfter(this.el);
+ $('.save', this.html_form_actions)
+ .text(this._('Save order'))
+ .on('click', this._onHandleSave);
+ $('.cancel', this.html_form_actions)
+ .text(this._('Cancel'))
+ .on('click', this._onHandleCancel);
+
+ this.html_handles = $(this.template.handle)
+ .hide()
+ .appendTo($('li', this.el));
+
+ this.html_saving = $(this.template.saving)
+ .hide()
+ .insertBefore($('.save', this.html_form_actions));
+ $('span', this.html_saving).text(this._('Saving...'));
+
+ this.cache = this.el.html();
+
+ this.el
+ .sortable()
+ .sortable('disable');
+
+ },
+
+ _onHandleStartReorder: function() {
+ if (!this.is_reordering) {
+ this.html_form_actions
+ .add(this.html_handles)
+ .add(this.html_title)
+ .show();
+ this.el
+ .addClass('reordering')
+ .sortable('enable');
+ $('.page_primary_action').hide();
+ this.is_reordering = true;
+ }
+ },
+
+ _onHandleCancel: function() {
+ if (
+ this.is_reordering
+ && !$('.cancel', this.html_form_actions).hasClass('disabled')
+ ) {
+ this.reset();
+ this.is_reordering = false;
+ this.el.html(this.cache)
+ .sortable()
+ .sortable('disable');
+ this.html_handles = $('.handle', this.el);
+ }
+ },
+
+ _onHandleSave: function() {
+ if (!$('.save', this.html_form_actions).hasClass('disabled')) {
+ var module = this;
+ module.html_saving.show();
+ $('.save, .cancel', module.html_form_actions).addClass('disabled');
+ var order = [];
+ $('li', module.el).each(function() {
+ order.push($(this).data('id'));
+ });
+ module.sandbox.client.call('POST', 'resource_view_reorder', {
+ id: module.options.id,
+ order: order
+ }, function() {
+ module.html_saving.hide();
+ $('.save, .cancel', module.html_form_actions).removeClass('disabled');
+ module.cache = module.el.html();
+ module.reset();
+ module.is_reordering = false;
+ });
+ }
+ },
+
+ reset: function() {
+ this.html_form_actions
+ .add(this.html_handles)
+ .add(this.html_title)
+ .hide();
+ this.el
+ .removeClass('reordering')
+ .sortable('disable');
+ $('.page_primary_action').show();
+ }
+
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-reorder.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-reorder.min.js
new file mode 100644
index 00000000..6fcae188
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/resource-view-reorder.min.js
@@ -0,0 +1 @@
+this.ckan.module('resource-view-reorder',function($){return{options:{id:false,labelText:'Reorder resource view'},template:{title:' ',button:['',' ',' ',' '].join('\n'),form_actions:[''].join('\n'),handle:['',' ',' '].join('\n'),saving:['',' ',' ',' '].join('\n')},is_reordering:false,cache:false,initialize:function(){jQuery.proxyAll(this,/_on/);var labelText=this._(this.options.labelText);this.html_title=$(this.template.title).text(labelText).insertBefore(this.el).hide();var button=$(this.template.button).on('click',this._onHandleStartReorder).appendTo('.page_primary_action');$('span',button).text(labelText);this.html_form_actions=$(this.template.form_actions).hide().insertAfter(this.el);$('.save',this.html_form_actions).text(this._('Save order')).on('click',this._onHandleSave);$('.cancel',this.html_form_actions).text(this._('Cancel')).on('click',this._onHandleCancel);this.html_handles=$(this.template.handle).hide().appendTo($('li',this.el));this.html_saving=$(this.template.saving).hide().insertBefore($('.save',this.html_form_actions));$('span',this.html_saving).text(this._('Saving...'));this.cache=this.el.html();this.el.sortable().sortable('disable');},_onHandleStartReorder:function(){if(!this.is_reordering){this.html_form_actions.add(this.html_handles).add(this.html_title).show();this.el.addClass('reordering').sortable('enable');$('.page_primary_action').hide();this.is_reordering=true;}},_onHandleCancel:function(){if(this.is_reordering&&!$('.cancel',this.html_form_actions).hasClass('disabled')){this.reset();this.is_reordering=false;this.el.html(this.cache).sortable().sortable('disable');this.html_handles=$('.handle',this.el);}},_onHandleSave:function(){if(!$('.save',this.html_form_actions).hasClass('disabled')){var module=this;module.html_saving.show();$('.save, .cancel',module.html_form_actions).addClass('disabled');var order=[];$('li',module.el).each(function(){order.push($(this).data('id'));});module.sandbox.client.call('POST','resource_view_reorder',{id:module.options.id,order:order},function(){module.html_saving.hide();$('.save, .cancel',module.html_form_actions).removeClass('disabled');module.cache=module.el.html();module.reset();module.is_reordering=false;});}},reset:function(){this.html_form_actions.add(this.html_handles).add(this.html_title).hide();this.el.removeClass('reordering').sortable('disable');$('.page_primary_action').show();}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/select-switch.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/select-switch.js
new file mode 100644
index 00000000..145b33a4
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/select-switch.js
@@ -0,0 +1,31 @@
+/* Finds the nearest select box in a form and watches it for changes. When
+ * a change occurs it submits the form. It can also hide the submit button if
+ * required.
+ *
+ * target - A selector to watch for changes (default: select)
+ * button - A selector for the button to hide in the form.
+ *
+ * Examples
+ *
+ *
+ *
+ * Returns .
+ */
+this.ckan.module('select-switch', {
+
+ options: {
+ target: 'select'
+ },
+
+ initialize: function () {
+ var _this = this;
+
+ this.el.on('change', this.options.target, function () {
+ _this.el.submit();
+ });
+ }
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/select-switch.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/select-switch.min.js
new file mode 100644
index 00000000..b0247c1f
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/select-switch.min.js
@@ -0,0 +1 @@
+this.ckan.module('select-switch',{options:{target:'select'},initialize:function(){var _this=this;this.el.on('change',this.options.target,function(){_this.el.submit();});}});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/slug-preview.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/slug-preview.js
new file mode 100644
index 00000000..7036b15f
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/slug-preview.js
@@ -0,0 +1,79 @@
+this.ckan.module('slug-preview-target', {
+ initialize: function () {
+ var sandbox = this.sandbox;
+ var options = this.options;
+ var el = this.el;
+
+ sandbox.subscribe('slug-preview-created', function (preview) {
+ // Append the preview string after the target input.
+ el.after(preview);
+ });
+
+ // Make sure there isn't a value in the field already...
+ if (el.val() == '') {
+ // Once the preview box is modified stop watching it.
+ sandbox.subscribe('slug-preview-modified', function () {
+ el.off('.slug-preview');
+ });
+
+ // Watch for updates to the target field and update the hidden slug field
+ // triggering the "change" event manually.
+ el.on('keyup.slug-preview input.slug-preview', function (event) {
+ sandbox.publish('slug-target-changed', this.value);
+ //slug.val(this.value).trigger('change');
+ });
+ }
+ }
+});
+
+this.ckan.module('slug-preview-slug', function (jQuery) {
+ return {
+ options: {
+ prefix: '',
+ placeholder: ''
+ },
+
+ initialize: function () {
+ var sandbox = this.sandbox;
+ var options = this.options;
+ var el = this.el;
+ var _ = sandbox.translate;
+
+ var slug = el.slug();
+ var parent = slug.parents('.form-group');
+ var preview;
+
+ if (!(parent.length)) {
+ return;
+ }
+
+ // Leave the slug field visible
+ if (!parent.hasClass('error')) {
+ preview = parent.slugPreview({
+ prefix: options.prefix,
+ placeholder: options.placeholder,
+ i18n: {
+ 'URL': this._('URL'),
+ 'Edit': this._('Edit')
+ }
+ });
+
+ // If the user manually enters text into the input we cancel the slug
+ // listeners so that we don't clobber the slug when the title next changes.
+ slug.keypress(function () {
+ if (event.charCode) {
+ sandbox.publish('slug-preview-modified', preview[0]);
+ }
+ });
+
+ sandbox.publish('slug-preview-created', preview[0]);
+ }
+
+ // Watch for updates to the target field and update the hidden slug field
+ // triggering the "change" event manually.
+ sandbox.subscribe('slug-target-changed', function (value) {
+ slug.val(value).trigger('change');
+ });
+ }
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/slug-preview.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/slug-preview.min.js
new file mode 100644
index 00000000..777fb3b2
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/slug-preview.min.js
@@ -0,0 +1,3 @@
+this.ckan.module('slug-preview-target',{initialize:function(){var sandbox=this.sandbox;var options=this.options;var el=this.el;sandbox.subscribe('slug-preview-created',function(preview){el.after(preview);});if(el.val()==''){sandbox.subscribe('slug-preview-modified',function(){el.off('.slug-preview');});el.on('keyup.slug-preview input.slug-preview',function(event){sandbox.publish('slug-target-changed',this.value);});}}});this.ckan.module('slug-preview-slug',function(jQuery){return{options:{prefix:'',placeholder:''},initialize:function(){var sandbox=this.sandbox;var options=this.options;var el=this.el;var _=sandbox.translate;var slug=el.slug();var parent=slug.parents('.form-group');var preview;if(!(parent.length)){return;}
+if(!parent.hasClass('error')){preview=parent.slugPreview({prefix:options.prefix,placeholder:options.placeholder,i18n:{'URL':this._('URL'),'Edit':this._('Edit')}});slug.keypress(function(){if(event.charCode){sandbox.publish('slug-preview-modified',preview[0]);}});sandbox.publish('slug-preview-created',preview[0]);}
+sandbox.subscribe('slug-target-changed',function(value){slug.val(value).trigger('change');});}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-selectable-rows.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-selectable-rows.js
new file mode 100644
index 00000000..4a1965ad
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-selectable-rows.js
@@ -0,0 +1,95 @@
+/* Table Selectable Rows
+ * Put's a select box in the of a and makes all rows
+ * selectable.
+ *
+ * Examples
+ *
+ *
+ *
+ */
+this.ckan.module('table-selectable-rows', function($) {
+ return {
+
+ // Store for jQuery object for the select all checkbox
+ select_all: null,
+ // Total number of checkboxes in the table (used for checking later)
+ total_checkboxes: 0,
+ // Store for jQuery object of all table header buttons
+ buttons: null,
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function() {
+ $.proxyAll(this, /_on/);
+ this.total_checkboxes = $('input[type="checkbox"]', this.el).length;
+ this.select_all = $(' ')
+ .data('select-all', true)
+ .appendTo($('thead th:first-child', this.el));
+ this.el.on('change', 'input[type="checkbox"]', this._onHandleCheckboxToggle);
+ this.buttons = $('th.actions .btn', this.el).addClass('disabled').prop('disabled', true);
+ },
+
+ /* Gets called whenever a user changes the :checked state on a checkbox
+ * within the table
+ *
+ * $e - jQuery event object
+ *
+ * Returns nothing.
+ */
+ _onHandleCheckboxToggle: function($e) {
+ var checkbox = $($e.target);
+ if (checkbox.data('select-all')) {
+ this.handleSelectAll(checkbox, checkbox.is(':checked'));
+ } else {
+ this.handleSelectOne(checkbox, checkbox.is(':checked'));
+ }
+ },
+
+ /* Handles the checking of all row
+ *
+ * $target - jQuery checkbox object
+ * $checked - Boolean of whether $target is checked
+ *
+ * Returns nothing.
+ */
+ handleSelectAll: function($target, $checked) {
+ $('input[type="checkbox"]', this.el).prop('checked', $checked);
+ if ($checked) {
+ $('tbody tr', this.el).addClass('table-selected');
+ this.buttons.removeClass('disabled').prop('disabled', false);
+ } else {
+ $('tbody tr', this.el).removeClass('table-selected');
+ this.buttons.addClass('disabled').prop('disabled', true);
+ }
+ },
+
+ /* Handles the checking of a single row
+ *
+ * $target - jQuery checkbox object
+ * $checked - Boolean of whether $target is checked
+ *
+ * Returns nothing.
+ */
+ handleSelectOne: function($target, $checked) {
+ if ($checked) {
+ $target.parents('tr').addClass('table-selected');
+ } else {
+ $target.parents('tr').removeClass('table-selected');
+ }
+ var checked = $('tbody input[type="checkbox"]:checked', this.el).length;
+ if (checked >= this.total_checkboxes) {
+ this.select_all.prop('checked', true);
+ } else {
+ this.select_all.prop('checked', false);
+ }
+ if (checked > 0) {
+ this.buttons.removeClass('disabled').prop('disabled', false);
+ } else {
+ this.buttons.addClass('disabled').prop('disabled', true);
+ }
+ }
+
+ };
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-selectable-rows.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-selectable-rows.min.js
new file mode 100644
index 00000000..765fb096
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-selectable-rows.min.js
@@ -0,0 +1,3 @@
+this.ckan.module('table-selectable-rows',function($){return{select_all:null,total_checkboxes:0,buttons:null,initialize:function(){$.proxyAll(this,/_on/);this.total_checkboxes=$('input[type="checkbox"]',this.el).length;this.select_all=$(' ').data('select-all',true).appendTo($('thead th:first-child',this.el));this.el.on('change','input[type="checkbox"]',this._onHandleCheckboxToggle);this.buttons=$('th.actions .btn',this.el).addClass('disabled').prop('disabled',true);},_onHandleCheckboxToggle:function($e){var checkbox=$($e.target);if(checkbox.data('select-all')){this.handleSelectAll(checkbox,checkbox.is(':checked'));}else{this.handleSelectOne(checkbox,checkbox.is(':checked'));}},handleSelectAll:function($target,$checked){$('input[type="checkbox"]',this.el).prop('checked',$checked);if($checked){$('tbody tr',this.el).addClass('table-selected');this.buttons.removeClass('disabled').prop('disabled',false);}else{$('tbody tr',this.el).removeClass('table-selected');this.buttons.addClass('disabled').prop('disabled',true);}},handleSelectOne:function($target,$checked){if($checked){$target.parents('tr').addClass('table-selected');}else{$target.parents('tr').removeClass('table-selected');}
+var checked=$('tbody input[type="checkbox"]:checked',this.el).length;if(checked>=this.total_checkboxes){this.select_all.prop('checked',true);}else{this.select_all.prop('checked',false);}
+if(checked>0){this.buttons.removeClass('disabled').prop('disabled',false);}else{this.buttons.addClass('disabled').prop('disabled',true);}}};});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-toggle-more.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-toggle-more.js
new file mode 100644
index 00000000..7a5c49c3
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-toggle-more.js
@@ -0,0 +1,61 @@
+/* Table toggle more
+ * When a table has more things to it that need to be hidden and then shown more
+ */
+this.ckan.module('table-toggle-more', function($) {
+ return {
+ /* options object can be extended using data-module-* attributes */
+ options: {},
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ $.proxyAll(this, /_on/);
+ this.el.addClass('table-toggle-more');
+ // Do we actually want this table to expand?
+ var rows = $('.toggle-more', this.el).length;
+ if (rows) {
+ // How much is the colspan?
+ var cols = $('thead tr th', this.el).length;
+ var template_more = [
+ '',
+ '',
+ '',
+ '' + this._('Show more') + ' ',
+ '' + this._('Hide') + ' ',
+ ' ',
+ ' ',
+ ' '
+ ].join('\n');
+ var template_seperator = [
+ '',
+ '',
+ ' ',
+ ' '
+ ].join('\n');
+
+ var seperator = $(template_seperator).insertAfter($('.toggle-more:last-child', this.el));
+ $(template_more).insertAfter(seperator);
+
+ $('.show-more', this.el).on('click', this._onShowMore);
+ $('.show-less', this.el).on('click', this._onShowLess);
+ }
+ },
+
+ _onShowMore: function($e) {
+ $e.preventDefault();
+ this.el
+ .removeClass('table-toggle-more')
+ .addClass('table-toggle-less');
+ },
+
+ _onShowLess: function($e) {
+ $e.preventDefault();
+ this.el
+ .removeClass('table-toggle-less')
+ .addClass('table-toggle-more');
+ }
+
+ }
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-toggle-more.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-toggle-more.min.js
new file mode 100644
index 00000000..e846d3f3
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/modules/table-toggle-more.min.js
@@ -0,0 +1 @@
+this.ckan.module('table-toggle-more',function($){return{options:{},initialize:function(){$.proxyAll(this,/_on/);this.el.addClass('table-toggle-more');var rows=$('.toggle-more',this.el).length;if(rows){var cols=$('thead tr th',this.el).length;var template_more=['','','',''+this._('Show more')+' ',''+this._('Hide')+' ',' ',' ',' '].join('\n');var template_seperator=['','',' ',' '].join('\n');var seperator=$(template_seperator).insertAfter($('.toggle-more:last-child',this.el));$(template_more).insertAfter(seperator);$('.show-more',this.el).on('click',this._onShowMore);$('.show-less',this.el).on('click',this._onShowLess);}},_onShowMore:function($e){$e.preventDefault();this.el.removeClass('table-toggle-more').addClass('table-toggle-less');},_onShowLess:function($e){$e.preventDefault();this.el.removeClass('table-toggle-less').addClass('table-toggle-more');}}});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.date-helpers.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.date-helpers.js
new file mode 100644
index 00000000..45d48a52
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.date-helpers.js
@@ -0,0 +1,82 @@
+this.jQuery.date = {
+ /* A map of date methods to text strings. */
+ METHODS: {
+ "yyyy": "getUTCFullYear",
+ "MM": "getUTCMonth",
+ "dd": "getUTCDate",
+ "HH": "getUTCHours",
+ "mm": "getUTCMinutes",
+ "ss": "getUTCSeconds",
+ "fff": "getUTCMilliseconds"
+ },
+
+ /* Formatting of an ISO8601 compatible date */
+ ISO8601: "yyyy-MM-ddTHH:mm:ss.fffZ",
+
+ /* Formatting of a CKAN compatible ISO string. See helpers.py */
+ CKAN8601: "yyyy-MM-ddTHH:mm:ss",
+
+ /* Returns a date string for the format provided.
+ *
+ * format - A format string in the form "yyyy-MM-dd"
+ * date - A date object to output.
+ *
+ * Returns a formatted date string.
+ */
+ format: function (format, date) {
+ var map = this.METHODS;
+
+ date = date || new Date();
+
+ function pad(str, exp) {
+ str = "" + str;
+ exp = exp.replace(/[a-z]/ig, '0');
+ return str.length !== exp.length ? exp.slice(str.length) + str : str;
+ }
+
+ return format.replace(/([a-zA-Z])\1+/g, function (_, $1) {
+ if (map[_]) {
+ var value = date[map[_]]();
+ if (_ === 'MM') {
+ value += 1;
+ }
+ return pad(value, _);
+ }
+ return _;
+ });
+ },
+
+ /* Generates a CKAN friendly ISO8601 timestamp.
+ *
+ * date - A date object to convert.
+ *
+ * Examples
+ *
+ * var timestamp = jQuery.date.toCKANString(new Date());
+ *
+ * Returns a timestamp string.
+ */
+ toCKANString: function (date) {
+ return this.format(this.CKAN8601, date);
+ },
+
+ /* Generates a ISO8601 timestamp. Uses the native methods if available.
+ *
+ * date - A date object to convert.
+ *
+ * Examples
+ *
+ * var timestamp = jQuery.date.toISOString(new Date());
+ *
+ * Returns a timestamp string.
+ */
+ toISOString: function (date) {
+ date = date || new Date();
+
+ if (date.toISOString) {
+ return date.toISOString();
+ } else {
+ return this.format(this.ISO8601, date);
+ }
+ }
+};
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.date-helpers.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.date-helpers.min.js
new file mode 100644
index 00000000..aede7abe
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.date-helpers.min.js
@@ -0,0 +1,4 @@
+this.jQuery.date={METHODS:{"yyyy":"getUTCFullYear","MM":"getUTCMonth","dd":"getUTCDate","HH":"getUTCHours","mm":"getUTCMinutes","ss":"getUTCSeconds","fff":"getUTCMilliseconds"},ISO8601:"yyyy-MM-ddTHH:mm:ss.fffZ",CKAN8601:"yyyy-MM-ddTHH:mm:ss",format:function(format,date){var map=this.METHODS;date=date||new Date();function pad(str,exp){str=""+str;exp=exp.replace(/[a-z]/ig,'0');return str.length!==exp.length?exp.slice(str.length)+str:str;}
+return format.replace(/([a-zA-Z])\1+/g,function(_,$1){if(map[_]){var value=date[map[_]]();if(_==='MM'){value+=1;}
+return pad(value,_);}
+return _;});},toCKANString:function(date){return this.format(this.CKAN8601,date);},toISOString:function(date){date=date||new Date();if(date.toISOString){return date.toISOString();}else{return this.format(this.ISO8601,date);}}};
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.form-warning.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.form-warning.js
new file mode 100644
index 00000000..11fc993a
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.form-warning.js
@@ -0,0 +1,41 @@
+(function (jQuery) {
+ /* Accepts a form element and once changed binds an event handler to the
+ * window "beforeunload" event that warns a user that the form has unsaved
+ * changes. The notice is only displayed if the user does not submit the
+ * form.
+ *
+ * message - A message to display to the user (browser support dependant).
+ *
+ * Examples
+ *
+ * jQuery('form').incompleteFormWarning('Form has modified fields');
+ *
+ * Returns the jQuery collection.
+ */
+ jQuery.fn.incompleteFormWarning = function (message) {
+ return this.each(function () {
+ var form = jQuery(this);
+ var state = form.serialize();
+
+ function onWindowUnload(event) {
+ if (event.originalEvent.returnValue) {
+ event.originalEvent.returnValue = message;
+ }
+ return message;
+ }
+
+ form.on({
+ change: function () {
+ // See if the form has changed, if so add an event listener otherwise
+ // remove it.
+ var method = form.serialize() === state ? 'off' : 'on';
+ jQuery(window)[method]('beforeunload', onWindowUnload);
+ },
+ submit: function () {
+ // Allow the form to be submitted.
+ jQuery(window).off('beforeunload', onWindowUnload);
+ }
+ });
+ });
+ };
+})(this.jQuery);
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.form-warning.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.form-warning.min.js
new file mode 100644
index 00000000..13d95e27
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.form-warning.min.js
@@ -0,0 +1,3 @@
+(function(jQuery){jQuery.fn.incompleteFormWarning=function(message){return this.each(function(){var form=jQuery(this);var state=form.serialize();function onWindowUnload(event){if(event.originalEvent.returnValue){event.originalEvent.returnValue=message;}
+return message;}
+form.on({change:function(){var method=form.serialize()===state?'off':'on';jQuery(window)[method]('beforeunload',onWindowUnload);},submit:function(){jQuery(window).off('beforeunload',onWindowUnload);}});});};})(this.jQuery);
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.images-loaded.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.images-loaded.js
new file mode 100644
index 00000000..f6ac95c2
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.images-loaded.js
@@ -0,0 +1,496 @@
+/*!
+ * imagesLoaded PACKAGED v4.1.4
+ * JavaScript is all like "You images are done yet or what?"
+ * MIT License
+ */
+
+/**
+ * EvEmitter v1.1.0
+ * Lil' event emitter
+ * MIT License
+ */
+
+/* jshint unused: true, undef: true, strict: true */
+
+( function( global, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /* globals define, module, window */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD - RequireJS
+ define( 'ev-emitter/ev-emitter',factory );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS - Browserify, Webpack
+ module.exports = factory();
+ } else {
+ // Browser globals
+ global.EvEmitter = factory();
+ }
+
+}( typeof window != 'undefined' ? window : this, function() {
+
+
+
+function EvEmitter() {}
+
+var proto = EvEmitter.prototype;
+
+proto.on = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // set events hash
+ var events = this._events = this._events || {};
+ // set listeners array
+ var listeners = events[ eventName ] = events[ eventName ] || [];
+ // only add once
+ if ( listeners.indexOf( listener ) == -1 ) {
+ listeners.push( listener );
+ }
+
+ return this;
+};
+
+proto.once = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // add event
+ this.on( eventName, listener );
+ // set once flag
+ // set onceEvents hash
+ var onceEvents = this._onceEvents = this._onceEvents || {};
+ // set onceListeners object
+ var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
+ // set flag
+ onceListeners[ listener ] = true;
+
+ return this;
+};
+
+proto.off = function( eventName, listener ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ var index = listeners.indexOf( listener );
+ if ( index != -1 ) {
+ listeners.splice( index, 1 );
+ }
+
+ return this;
+};
+
+proto.emitEvent = function( eventName, args ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ // copy over to avoid interference if .off() in listener
+ listeners = listeners.slice(0);
+ args = args || [];
+ // once stuff
+ var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
+
+ for ( var i=0; i < listeners.length; i++ ) {
+ var listener = listeners[i]
+ var isOnce = onceListeners && onceListeners[ listener ];
+ if ( isOnce ) {
+ // remove listener
+ // remove before trigger to prevent recursion
+ this.off( eventName, listener );
+ // unset once flag
+ delete onceListeners[ listener ];
+ }
+ // trigger listener
+ listener.apply( this, args );
+ }
+
+ return this;
+};
+
+proto.allOff = function() {
+ delete this._events;
+ delete this._onceEvents;
+};
+
+return EvEmitter;
+
+}));
+
+/*!
+ * imagesLoaded v4.1.4
+ * JavaScript is all like "You images are done yet or what?"
+ * MIT License
+ */
+
+( function( window, factory ) { 'use strict';
+ // universal module definition
+
+ /*global define: false, module: false, require: false */
+
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( [
+ 'ev-emitter/ev-emitter'
+ ], function( EvEmitter ) {
+ return factory( window, EvEmitter );
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ window,
+ require('ev-emitter')
+ );
+ } else {
+ // browser global
+ window.imagesLoaded = factory(
+ window,
+ window.EvEmitter
+ );
+ }
+
+})( typeof window !== 'undefined' ? window : this,
+
+// -------------------------- factory -------------------------- //
+
+function factory( window, EvEmitter ) {
+
+
+
+var $ = window.jQuery;
+var console = window.console;
+
+// -------------------------- helpers -------------------------- //
+
+// extend objects
+function extend( a, b ) {
+ for ( var prop in b ) {
+ a[ prop ] = b[ prop ];
+ }
+ return a;
+}
+
+var arraySlice = Array.prototype.slice;
+
+// turn element or nodeList into an array
+function makeArray( obj ) {
+ if ( Array.isArray( obj ) ) {
+ // use object if already an array
+ return obj;
+ }
+
+ var isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
+ if ( isArrayLike ) {
+ // convert nodeList to array
+ return arraySlice.call( obj );
+ }
+
+ // array of single index
+ return [ obj ];
+}
+
+// -------------------------- imagesLoaded -------------------------- //
+
+/**
+ * @param {Array, Element, NodeList, String} elem
+ * @param {Object or Function} options - if function, use as callback
+ * @param {Function} onAlways - callback function
+ */
+function ImagesLoaded( elem, options, onAlways ) {
+ // coerce ImagesLoaded() without new, to be new ImagesLoaded()
+ if ( !( this instanceof ImagesLoaded ) ) {
+ return new ImagesLoaded( elem, options, onAlways );
+ }
+ // use elem as selector string
+ var queryElem = elem;
+ if ( typeof elem == 'string' ) {
+ queryElem = document.querySelectorAll( elem );
+ }
+ // bail if bad element
+ if ( !queryElem ) {
+ console.error( 'Bad element for imagesLoaded ' + ( queryElem || elem ) );
+ return;
+ }
+
+ this.elements = makeArray( queryElem );
+ this.options = extend( {}, this.options );
+ // shift arguments if no options set
+ if ( typeof options == 'function' ) {
+ onAlways = options;
+ } else {
+ extend( this.options, options );
+ }
+
+ if ( onAlways ) {
+ this.on( 'always', onAlways );
+ }
+
+ this.getImages();
+
+ if ( $ ) {
+ // add jQuery Deferred object
+ this.jqDeferred = new $.Deferred();
+ }
+
+ // HACK check async to allow time to bind listeners
+ setTimeout( this.check.bind( this ) );
+}
+
+ImagesLoaded.prototype = Object.create( EvEmitter.prototype );
+
+ImagesLoaded.prototype.options = {};
+
+ImagesLoaded.prototype.getImages = function() {
+ this.images = [];
+
+ // filter & find items if we have an item selector
+ this.elements.forEach( this.addElementImages, this );
+};
+
+/**
+ * @param {Node} element
+ */
+ImagesLoaded.prototype.addElementImages = function( elem ) {
+ // filter siblings
+ if ( elem.nodeName == 'IMG' ) {
+ this.addImage( elem );
+ }
+ // get background image on element
+ if ( this.options.background === true ) {
+ this.addElementBackgroundImages( elem );
+ }
+
+ // find children
+ // no non-element nodes, #143
+ var nodeType = elem.nodeType;
+ if ( !nodeType || !elementNodeTypes[ nodeType ] ) {
+ return;
+ }
+ var childImgs = elem.querySelectorAll('img');
+ // concat childElems to filterFound array
+ for ( var i=0; i < childImgs.length; i++ ) {
+ var img = childImgs[i];
+ this.addImage( img );
+ }
+
+ // get child background images
+ if ( typeof this.options.background == 'string' ) {
+ var children = elem.querySelectorAll( this.options.background );
+ for ( i=0; i < children.length; i++ ) {
+ var child = children[i];
+ this.addElementBackgroundImages( child );
+ }
+ }
+};
+
+var elementNodeTypes = {
+ 1: true,
+ 9: true,
+ 11: true
+};
+
+ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
+ var style = getComputedStyle( elem );
+ if ( !style ) {
+ // Firefox returns null if in a hidden iframe https://bugzil.la/548397
+ return;
+ }
+ // get url inside url("...")
+ var reURL = /url\((['"])?(.*?)\1\)/gi;
+ var matches = reURL.exec( style.backgroundImage );
+ while ( matches !== null ) {
+ var url = matches && matches[2];
+ if ( url ) {
+ this.addBackground( url, elem );
+ }
+ matches = reURL.exec( style.backgroundImage );
+ }
+};
+
+/**
+ * @param {Image} img
+ */
+ImagesLoaded.prototype.addImage = function( img ) {
+ var loadingImage = new LoadingImage( img );
+ this.images.push( loadingImage );
+};
+
+ImagesLoaded.prototype.addBackground = function( url, elem ) {
+ var background = new Background( url, elem );
+ this.images.push( background );
+};
+
+ImagesLoaded.prototype.check = function() {
+ var _this = this;
+ this.progressedCount = 0;
+ this.hasAnyBroken = false;
+ // complete if no images
+ if ( !this.images.length ) {
+ this.complete();
+ return;
+ }
+
+ function onProgress( image, elem, message ) {
+ // HACK - Chrome triggers event before object properties have changed. #83
+ setTimeout( function() {
+ _this.progress( image, elem, message );
+ });
+ }
+
+ this.images.forEach( function( loadingImage ) {
+ loadingImage.once( 'progress', onProgress );
+ loadingImage.check();
+ });
+};
+
+ImagesLoaded.prototype.progress = function( image, elem, message ) {
+ this.progressedCount++;
+ this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;
+ // progress event
+ this.emitEvent( 'progress', [ this, image, elem ] );
+ if ( this.jqDeferred && this.jqDeferred.notify ) {
+ this.jqDeferred.notify( this, image );
+ }
+ // check if completed
+ if ( this.progressedCount == this.images.length ) {
+ this.complete();
+ }
+
+ if ( this.options.debug && console ) {
+ console.log( 'progress: ' + message, image, elem );
+ }
+};
+
+ImagesLoaded.prototype.complete = function() {
+ var eventName = this.hasAnyBroken ? 'fail' : 'done';
+ this.isComplete = true;
+ this.emitEvent( eventName, [ this ] );
+ this.emitEvent( 'always', [ this ] );
+ if ( this.jqDeferred ) {
+ var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';
+ this.jqDeferred[ jqMethod ]( this );
+ }
+};
+
+// -------------------------- -------------------------- //
+
+function LoadingImage( img ) {
+ this.img = img;
+}
+
+LoadingImage.prototype = Object.create( EvEmitter.prototype );
+
+LoadingImage.prototype.check = function() {
+ // If complete is true and browser supports natural sizes,
+ // try to check for image status manually.
+ var isComplete = this.getIsImageComplete();
+ if ( isComplete ) {
+ // report based on naturalWidth
+ this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
+ return;
+ }
+
+ // If none of the checks above matched, simulate loading on detached element.
+ this.proxyImage = new Image();
+ this.proxyImage.addEventListener( 'load', this );
+ this.proxyImage.addEventListener( 'error', this );
+ // bind to image as well for Firefox. #191
+ this.img.addEventListener( 'load', this );
+ this.img.addEventListener( 'error', this );
+ this.proxyImage.src = this.img.src;
+};
+
+LoadingImage.prototype.getIsImageComplete = function() {
+ // check for non-zero, non-undefined naturalWidth
+ // fixes Safari+InfiniteScroll+Masonry bug infinite-scroll#671
+ return this.img.complete && this.img.naturalWidth;
+};
+
+LoadingImage.prototype.confirm = function( isLoaded, message ) {
+ this.isLoaded = isLoaded;
+ this.emitEvent( 'progress', [ this, this.img, message ] );
+};
+
+// ----- events ----- //
+
+// trigger specified handler for event type
+LoadingImage.prototype.handleEvent = function( event ) {
+ var method = 'on' + event.type;
+ if ( this[ method ] ) {
+ this[ method ]( event );
+ }
+};
+
+LoadingImage.prototype.onload = function() {
+ this.confirm( true, 'onload' );
+ this.unbindEvents();
+};
+
+LoadingImage.prototype.onerror = function() {
+ this.confirm( false, 'onerror' );
+ this.unbindEvents();
+};
+
+LoadingImage.prototype.unbindEvents = function() {
+ this.proxyImage.removeEventListener( 'load', this );
+ this.proxyImage.removeEventListener( 'error', this );
+ this.img.removeEventListener( 'load', this );
+ this.img.removeEventListener( 'error', this );
+};
+
+// -------------------------- Background -------------------------- //
+
+function Background( url, element ) {
+ this.url = url;
+ this.element = element;
+ this.img = new Image();
+}
+
+// inherit LoadingImage prototype
+Background.prototype = Object.create( LoadingImage.prototype );
+
+Background.prototype.check = function() {
+ this.img.addEventListener( 'load', this );
+ this.img.addEventListener( 'error', this );
+ this.img.src = this.url;
+ // check if image is already complete
+ var isComplete = this.getIsImageComplete();
+ if ( isComplete ) {
+ this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
+ this.unbindEvents();
+ }
+};
+
+Background.prototype.unbindEvents = function() {
+ this.img.removeEventListener( 'load', this );
+ this.img.removeEventListener( 'error', this );
+};
+
+Background.prototype.confirm = function( isLoaded, message ) {
+ this.isLoaded = isLoaded;
+ this.emitEvent( 'progress', [ this, this.element, message ] );
+};
+
+// -------------------------- jQuery -------------------------- //
+
+ImagesLoaded.makeJQueryPlugin = function( jQuery ) {
+ jQuery = jQuery || window.jQuery;
+ if ( !jQuery ) {
+ return;
+ }
+ // set local variable
+ $ = jQuery;
+ // $().imagesLoaded()
+ $.fn.imagesLoaded = function( options, callback ) {
+ var instance = new ImagesLoaded( this, options, callback );
+ return instance.jqDeferred.promise( $(this) );
+ };
+};
+// try making plugin
+ImagesLoaded.makeJQueryPlugin();
+
+// -------------------------- -------------------------- //
+
+return ImagesLoaded;
+
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.images-loaded.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.images-loaded.min.js
new file mode 100644
index 00000000..dd524bed
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.images-loaded.min.js
@@ -0,0 +1,37 @@
+(function(global,factory){if(typeof define=='function'&&define.amd){define('ev-emitter/ev-emitter',factory);}else if(typeof module=='object'&&module.exports){module.exports=factory();}else{global.EvEmitter=factory();}}(typeof window!='undefined'?window:this,function(){function EvEmitter(){}
+var proto=EvEmitter.prototype;proto.on=function(eventName,listener){if(!eventName||!listener){return;}
+var events=this._events=this._events||{};var listeners=events[eventName]=events[eventName]||[];if(listeners.indexOf(listener)==-1){listeners.push(listener);}
+return this;};proto.once=function(eventName,listener){if(!eventName||!listener){return;}
+this.on(eventName,listener);var onceEvents=this._onceEvents=this._onceEvents||{};var onceListeners=onceEvents[eventName]=onceEvents[eventName]||{};onceListeners[listener]=true;return this;};proto.off=function(eventName,listener){var listeners=this._events&&this._events[eventName];if(!listeners||!listeners.length){return;}
+var index=listeners.indexOf(listener);if(index!=-1){listeners.splice(index,1);}
+return this;};proto.emitEvent=function(eventName,args){var listeners=this._events&&this._events[eventName];if(!listeners||!listeners.length){return;}
+listeners=listeners.slice(0);args=args||[];var onceListeners=this._onceEvents&&this._onceEvents[eventName];for(var i=0;i $().plugin('option', {...})
+ if ( !PluginClass.prototype.option ) {
+ // option setter
+ PluginClass.prototype.option = function( opts ) {
+ // bail out if not an object
+ if ( !$.isPlainObject( opts ) ){
+ return;
+ }
+ this.options = $.extend( true, this.options, opts );
+ };
+ }
+
+ // make jQuery plugin
+ $.fn[ namespace ] = function( arg0 /*, arg1 */ ) {
+ if ( typeof arg0 == 'string' ) {
+ // method call $().plugin( 'methodName', { options } )
+ // shift arguments by 1
+ var args = arraySlice.call( arguments, 1 );
+ return methodCall( this, arg0, args );
+ }
+ // just $().plugin({ options })
+ plainCall( this, arg0 );
+ return this;
+ };
+
+ // $().plugin('methodName')
+ function methodCall( $elems, methodName, args ) {
+ var returnValue;
+ var pluginMethodStr = '$().' + namespace + '("' + methodName + '")';
+
+ $elems.each( function( i, elem ) {
+ // get instance
+ var instance = $.data( elem, namespace );
+ if ( !instance ) {
+ logError( namespace + ' not initialized. Cannot call methods, i.e. ' +
+ pluginMethodStr );
+ return;
+ }
+
+ var method = instance[ methodName ];
+ if ( !method || methodName.charAt(0) == '_' ) {
+ logError( pluginMethodStr + ' is not a valid method' );
+ return;
+ }
+
+ // apply method, get return value
+ var value = method.apply( instance, args );
+ // set return value if value is returned, use only first value
+ returnValue = returnValue === undefined ? value : returnValue;
+ });
+
+ return returnValue !== undefined ? returnValue : $elems;
+ }
+
+ function plainCall( $elems, options ) {
+ $elems.each( function( i, elem ) {
+ var instance = $.data( elem, namespace );
+ if ( instance ) {
+ // set options & init
+ instance.option( options );
+ instance._init();
+ } else {
+ // initialize new instance
+ instance = new PluginClass( elem, options );
+ $.data( elem, namespace, instance );
+ }
+ });
+ }
+
+ updateJQuery( $ );
+
+}
+
+// ----- updateJQuery ----- //
+
+// set $.bridget for v1 backwards compatibility
+function updateJQuery( $ ) {
+ if ( !$ || ( $ && $.bridget ) ) {
+ return;
+ }
+ $.bridget = jQueryBridget;
+}
+
+updateJQuery( jQuery || window.jQuery );
+
+// ----- ----- //
+
+return jQueryBridget;
+
+}));
+
+/**
+ * EvEmitter v1.1.0
+ * Lil' event emitter
+ * MIT License
+ */
+
+/* jshint unused: true, undef: true, strict: true */
+
+( function( global, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /* globals define, module, window */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD - RequireJS
+ define( 'ev-emitter/ev-emitter',factory );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS - Browserify, Webpack
+ module.exports = factory();
+ } else {
+ // Browser globals
+ global.EvEmitter = factory();
+ }
+
+}( typeof window != 'undefined' ? window : this, function() {
+
+
+
+function EvEmitter() {}
+
+var proto = EvEmitter.prototype;
+
+proto.on = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // set events hash
+ var events = this._events = this._events || {};
+ // set listeners array
+ var listeners = events[ eventName ] = events[ eventName ] || [];
+ // only add once
+ if ( listeners.indexOf( listener ) == -1 ) {
+ listeners.push( listener );
+ }
+
+ return this;
+};
+
+proto.once = function( eventName, listener ) {
+ if ( !eventName || !listener ) {
+ return;
+ }
+ // add event
+ this.on( eventName, listener );
+ // set once flag
+ // set onceEvents hash
+ var onceEvents = this._onceEvents = this._onceEvents || {};
+ // set onceListeners object
+ var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
+ // set flag
+ onceListeners[ listener ] = true;
+
+ return this;
+};
+
+proto.off = function( eventName, listener ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ var index = listeners.indexOf( listener );
+ if ( index != -1 ) {
+ listeners.splice( index, 1 );
+ }
+
+ return this;
+};
+
+proto.emitEvent = function( eventName, args ) {
+ var listeners = this._events && this._events[ eventName ];
+ if ( !listeners || !listeners.length ) {
+ return;
+ }
+ // copy over to avoid interference if .off() in listener
+ listeners = listeners.slice(0);
+ args = args || [];
+ // once stuff
+ var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
+
+ for ( var i=0; i < listeners.length; i++ ) {
+ var listener = listeners[i]
+ var isOnce = onceListeners && onceListeners[ listener ];
+ if ( isOnce ) {
+ // remove listener
+ // remove before trigger to prevent recursion
+ this.off( eventName, listener );
+ // unset once flag
+ delete onceListeners[ listener ];
+ }
+ // trigger listener
+ listener.apply( this, args );
+ }
+
+ return this;
+};
+
+proto.allOff = function() {
+ delete this._events;
+ delete this._onceEvents;
+};
+
+return EvEmitter;
+
+}));
+
+/*!
+ * getSize v2.0.2
+ * measure size of elements
+ * MIT license
+ */
+
+/*jshint browser: true, strict: true, undef: true, unused: true */
+/*global define: false, module: false, console: false */
+
+( function( window, factory ) {
+ 'use strict';
+
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( 'get-size/get-size',[],function() {
+ return factory();
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory();
+ } else {
+ // browser global
+ window.getSize = factory();
+ }
+
+})( window, function factory() {
+'use strict';
+
+// -------------------------- helpers -------------------------- //
+
+// get a number from a string, not a percentage
+function getStyleSize( value ) {
+ var num = parseFloat( value );
+ // not a percent like '100%', and a number
+ var isValid = value.indexOf('%') == -1 && !isNaN( num );
+ return isValid && num;
+}
+
+function noop() {}
+
+var logError = typeof console == 'undefined' ? noop :
+ function( message ) {
+ console.error( message );
+ };
+
+// -------------------------- measurements -------------------------- //
+
+var measurements = [
+ 'paddingLeft',
+ 'paddingRight',
+ 'paddingTop',
+ 'paddingBottom',
+ 'marginLeft',
+ 'marginRight',
+ 'marginTop',
+ 'marginBottom',
+ 'borderLeftWidth',
+ 'borderRightWidth',
+ 'borderTopWidth',
+ 'borderBottomWidth'
+];
+
+var measurementsLength = measurements.length;
+
+function getZeroSize() {
+ var size = {
+ width: 0,
+ height: 0,
+ innerWidth: 0,
+ innerHeight: 0,
+ outerWidth: 0,
+ outerHeight: 0
+ };
+ for ( var i=0; i < measurementsLength; i++ ) {
+ var measurement = measurements[i];
+ size[ measurement ] = 0;
+ }
+ return size;
+}
+
+// -------------------------- getStyle -------------------------- //
+
+/**
+ * getStyle, get style of element, check for Firefox bug
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=548397
+ */
+function getStyle( elem ) {
+ var style = getComputedStyle( elem );
+ if ( !style ) {
+ logError( 'Style returned ' + style +
+ '. Are you running this code in a hidden iframe on Firefox? ' +
+ 'See http://bit.ly/getsizebug1' );
+ }
+ return style;
+}
+
+// -------------------------- setup -------------------------- //
+
+var isSetup = false;
+
+var isBoxSizeOuter;
+
+/**
+ * setup
+ * check isBoxSizerOuter
+ * do on first getSize() rather than on page load for Firefox bug
+ */
+function setup() {
+ // setup once
+ if ( isSetup ) {
+ return;
+ }
+ isSetup = true;
+
+ // -------------------------- box sizing -------------------------- //
+
+ /**
+ * WebKit measures the outer-width on style.width on border-box elems
+ * IE & Firefox<29 measures the inner-width
+ */
+ var div = document.createElement('div');
+ div.style.width = '200px';
+ div.style.padding = '1px 2px 3px 4px';
+ div.style.borderStyle = 'solid';
+ div.style.borderWidth = '1px 2px 3px 4px';
+ div.style.boxSizing = 'border-box';
+
+ var body = document.body || document.documentElement;
+ body.appendChild( div );
+ var style = getStyle( div );
+
+ getSize.isBoxSizeOuter = isBoxSizeOuter = getStyleSize( style.width ) == 200;
+ body.removeChild( div );
+
+}
+
+// -------------------------- getSize -------------------------- //
+
+function getSize( elem ) {
+ setup();
+
+ // use querySeletor if elem is string
+ if ( typeof elem == 'string' ) {
+ elem = document.querySelector( elem );
+ }
+
+ // do not proceed on non-objects
+ if ( !elem || typeof elem != 'object' || !elem.nodeType ) {
+ return;
+ }
+
+ var style = getStyle( elem );
+
+ // if hidden, everything is 0
+ if ( style.display == 'none' ) {
+ return getZeroSize();
+ }
+
+ var size = {};
+ size.width = elem.offsetWidth;
+ size.height = elem.offsetHeight;
+
+ var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box';
+
+ // get all measurements
+ for ( var i=0; i < measurementsLength; i++ ) {
+ var measurement = measurements[i];
+ var value = style[ measurement ];
+ var num = parseFloat( value );
+ // any 'auto', 'medium' value will be 0
+ size[ measurement ] = !isNaN( num ) ? num : 0;
+ }
+
+ var paddingWidth = size.paddingLeft + size.paddingRight;
+ var paddingHeight = size.paddingTop + size.paddingBottom;
+ var marginWidth = size.marginLeft + size.marginRight;
+ var marginHeight = size.marginTop + size.marginBottom;
+ var borderWidth = size.borderLeftWidth + size.borderRightWidth;
+ var borderHeight = size.borderTopWidth + size.borderBottomWidth;
+
+ var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter;
+
+ // overwrite width and height if we can get it from style
+ var styleWidth = getStyleSize( style.width );
+ if ( styleWidth !== false ) {
+ size.width = styleWidth +
+ // add padding and border unless it's already including it
+ ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth );
+ }
+
+ var styleHeight = getStyleSize( style.height );
+ if ( styleHeight !== false ) {
+ size.height = styleHeight +
+ // add padding and border unless it's already including it
+ ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight );
+ }
+
+ size.innerWidth = size.width - ( paddingWidth + borderWidth );
+ size.innerHeight = size.height - ( paddingHeight + borderHeight );
+
+ size.outerWidth = size.width + marginWidth;
+ size.outerHeight = size.height + marginHeight;
+
+ return size;
+}
+
+return getSize;
+
+});
+
+/**
+ * matchesSelector v2.0.2
+ * matchesSelector( element, '.selector' )
+ * MIT license
+ */
+
+/*jshint browser: true, strict: true, undef: true, unused: true */
+
+( function( window, factory ) {
+ /*global define: false, module: false */
+ 'use strict';
+ // universal module definition
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( 'desandro-matches-selector/matches-selector',factory );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory();
+ } else {
+ // browser global
+ window.matchesSelector = factory();
+ }
+
+}( window, function factory() {
+ 'use strict';
+
+ var matchesMethod = ( function() {
+ var ElemProto = window.Element.prototype;
+ // check for the standard method name first
+ if ( ElemProto.matches ) {
+ return 'matches';
+ }
+ // check un-prefixed
+ if ( ElemProto.matchesSelector ) {
+ return 'matchesSelector';
+ }
+ // check vendor prefixes
+ var prefixes = [ 'webkit', 'moz', 'ms', 'o' ];
+
+ for ( var i=0; i < prefixes.length; i++ ) {
+ var prefix = prefixes[i];
+ var method = prefix + 'MatchesSelector';
+ if ( ElemProto[ method ] ) {
+ return method;
+ }
+ }
+ })();
+
+ return function matchesSelector( elem, selector ) {
+ return elem[ matchesMethod ]( selector );
+ };
+
+}));
+
+/**
+ * Fizzy UI utils v2.0.5
+ * MIT license
+ */
+
+/*jshint browser: true, undef: true, unused: true, strict: true */
+
+( function( window, factory ) {
+ // universal module definition
+ /*jshint strict: false */ /*globals define, module, require */
+
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( 'fizzy-ui-utils/utils',[
+ 'desandro-matches-selector/matches-selector'
+ ], function( matchesSelector ) {
+ return factory( window, matchesSelector );
+ });
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ window,
+ require('desandro-matches-selector')
+ );
+ } else {
+ // browser global
+ window.fizzyUIUtils = factory(
+ window,
+ window.matchesSelector
+ );
+ }
+
+}( window, function factory( window, matchesSelector ) {
+
+
+
+var utils = {};
+
+// ----- extend ----- //
+
+// extends objects
+utils.extend = function( a, b ) {
+ for ( var prop in b ) {
+ a[ prop ] = b[ prop ];
+ }
+ return a;
+};
+
+// ----- modulo ----- //
+
+utils.modulo = function( num, div ) {
+ return ( ( num % div ) + div ) % div;
+};
+
+// ----- makeArray ----- //
+
+// turn element or nodeList into an array
+utils.makeArray = function( obj ) {
+ var ary = [];
+ if ( Array.isArray( obj ) ) {
+ // use object if already an array
+ ary = obj;
+ } else if ( obj && typeof obj == 'object' &&
+ typeof obj.length == 'number' ) {
+ // convert nodeList to array
+ for ( var i=0; i < obj.length; i++ ) {
+ ary.push( obj[i] );
+ }
+ } else {
+ // array of single index
+ ary.push( obj );
+ }
+ return ary;
+};
+
+// ----- removeFrom ----- //
+
+utils.removeFrom = function( ary, obj ) {
+ var index = ary.indexOf( obj );
+ if ( index != -1 ) {
+ ary.splice( index, 1 );
+ }
+};
+
+// ----- getParent ----- //
+
+utils.getParent = function( elem, selector ) {
+ while ( elem.parentNode && elem != document.body ) {
+ elem = elem.parentNode;
+ if ( matchesSelector( elem, selector ) ) {
+ return elem;
+ }
+ }
+};
+
+// ----- getQueryElement ----- //
+
+// use element as selector string
+utils.getQueryElement = function( elem ) {
+ if ( typeof elem == 'string' ) {
+ return document.querySelector( elem );
+ }
+ return elem;
+};
+
+// ----- handleEvent ----- //
+
+// enable .ontype to trigger from .addEventListener( elem, 'type' )
+utils.handleEvent = function( event ) {
+ var method = 'on' + event.type;
+ if ( this[ method ] ) {
+ this[ method ]( event );
+ }
+};
+
+// ----- filterFindElements ----- //
+
+utils.filterFindElements = function( elems, selector ) {
+ // make array of elems
+ elems = utils.makeArray( elems );
+ var ffElems = [];
+
+ elems.forEach( function( elem ) {
+ // check that elem is an actual element
+ if ( !( elem instanceof HTMLElement ) ) {
+ return;
+ }
+ // add elem if no selector
+ if ( !selector ) {
+ ffElems.push( elem );
+ return;
+ }
+ // filter & find items if we have a selector
+ // filter
+ if ( matchesSelector( elem, selector ) ) {
+ ffElems.push( elem );
+ }
+ // find children
+ var childElems = elem.querySelectorAll( selector );
+ // concat childElems to filterFound array
+ for ( var i=0; i < childElems.length; i++ ) {
+ ffElems.push( childElems[i] );
+ }
+ });
+
+ return ffElems;
+};
+
+// ----- debounceMethod ----- //
+
+utils.debounceMethod = function( _class, methodName, threshold ) {
+ // original method
+ var method = _class.prototype[ methodName ];
+ var timeoutName = methodName + 'Timeout';
+
+ _class.prototype[ methodName ] = function() {
+ var timeout = this[ timeoutName ];
+ if ( timeout ) {
+ clearTimeout( timeout );
+ }
+ var args = arguments;
+
+ var _this = this;
+ this[ timeoutName ] = setTimeout( function() {
+ method.apply( _this, args );
+ delete _this[ timeoutName ];
+ }, threshold || 100 );
+ };
+};
+
+// ----- docReady ----- //
+
+utils.docReady = function( callback ) {
+ var readyState = document.readyState;
+ if ( readyState == 'complete' || readyState == 'interactive' ) {
+ // do async to allow for other scripts to run. metafizzy/flickity#441
+ setTimeout( callback );
+ } else {
+ document.addEventListener( 'DOMContentLoaded', callback );
+ }
+};
+
+// ----- htmlInit ----- //
+
+// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/
+utils.toDashed = function( str ) {
+ return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) {
+ return $1 + '-' + $2;
+ }).toLowerCase();
+};
+
+var console = window.console;
+/**
+ * allow user to initialize classes via [data-namespace] or .js-namespace class
+ * htmlInit( Widget, 'widgetName' )
+ * options are parsed from data-namespace-options
+ */
+utils.htmlInit = function( WidgetClass, namespace ) {
+ utils.docReady( function() {
+ var dashedNamespace = utils.toDashed( namespace );
+ var dataAttr = 'data-' + dashedNamespace;
+ var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' );
+ var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace );
+ var elems = utils.makeArray( dataAttrElems )
+ .concat( utils.makeArray( jsDashElems ) );
+ var dataOptionsAttr = dataAttr + '-options';
+ var jQuery = window.jQuery;
+
+ elems.forEach( function( elem ) {
+ var attr = elem.getAttribute( dataAttr ) ||
+ elem.getAttribute( dataOptionsAttr );
+ var options;
+ try {
+ options = attr && JSON.parse( attr );
+ } catch ( error ) {
+ // log error, do not initialize
+ if ( console ) {
+ console.error( 'Error parsing ' + dataAttr + ' on ' + elem.className +
+ ': ' + error );
+ }
+ return;
+ }
+ // initialize
+ var instance = new WidgetClass( elem, options );
+ // make available via $().data('namespace')
+ if ( jQuery ) {
+ jQuery.data( elem, namespace, instance );
+ }
+ });
+
+ });
+};
+
+// ----- ----- //
+
+return utils;
+
+}));
+
+/**
+ * Outlayer Item
+ */
+
+( function( window, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /* globals define, module, require */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD - RequireJS
+ define( 'outlayer/item',[
+ 'ev-emitter/ev-emitter',
+ 'get-size/get-size'
+ ],
+ factory
+ );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS - Browserify, Webpack
+ module.exports = factory(
+ require('ev-emitter'),
+ require('get-size')
+ );
+ } else {
+ // browser global
+ window.Outlayer = {};
+ window.Outlayer.Item = factory(
+ window.EvEmitter,
+ window.getSize
+ );
+ }
+
+}( window, function factory( EvEmitter, getSize ) {
+'use strict';
+
+// ----- helpers ----- //
+
+function isEmptyObj( obj ) {
+ for ( var prop in obj ) {
+ return false;
+ }
+ prop = null;
+ return true;
+}
+
+// -------------------------- CSS3 support -------------------------- //
+
+
+var docElemStyle = document.documentElement.style;
+
+var transitionProperty = typeof docElemStyle.transition == 'string' ?
+ 'transition' : 'WebkitTransition';
+var transformProperty = typeof docElemStyle.transform == 'string' ?
+ 'transform' : 'WebkitTransform';
+
+var transitionEndEvent = {
+ WebkitTransition: 'webkitTransitionEnd',
+ transition: 'transitionend'
+}[ transitionProperty ];
+
+// cache all vendor properties that could have vendor prefix
+var vendorProperties = {
+ transform: transformProperty,
+ transition: transitionProperty,
+ transitionDuration: transitionProperty + 'Duration',
+ transitionProperty: transitionProperty + 'Property',
+ transitionDelay: transitionProperty + 'Delay'
+};
+
+// -------------------------- Item -------------------------- //
+
+function Item( element, layout ) {
+ if ( !element ) {
+ return;
+ }
+
+ this.element = element;
+ // parent layout class, i.e. Masonry, Isotope, or Packery
+ this.layout = layout;
+ this.position = {
+ x: 0,
+ y: 0
+ };
+
+ this._create();
+}
+
+// inherit EvEmitter
+var proto = Item.prototype = Object.create( EvEmitter.prototype );
+proto.constructor = Item;
+
+proto._create = function() {
+ // transition objects
+ this._transn = {
+ ingProperties: {},
+ clean: {},
+ onEnd: {}
+ };
+
+ this.css({
+ position: 'absolute'
+ });
+};
+
+// trigger specified handler for event type
+proto.handleEvent = function( event ) {
+ var method = 'on' + event.type;
+ if ( this[ method ] ) {
+ this[ method ]( event );
+ }
+};
+
+proto.getSize = function() {
+ this.size = getSize( this.element );
+};
+
+/**
+ * apply CSS styles to element
+ * @param {Object} style
+ */
+proto.css = function( style ) {
+ var elemStyle = this.element.style;
+
+ for ( var prop in style ) {
+ // use vendor property if available
+ var supportedProp = vendorProperties[ prop ] || prop;
+ elemStyle[ supportedProp ] = style[ prop ];
+ }
+};
+
+ // measure position, and sets it
+proto.getPosition = function() {
+ var style = getComputedStyle( this.element );
+ var isOriginLeft = this.layout._getOption('originLeft');
+ var isOriginTop = this.layout._getOption('originTop');
+ var xValue = style[ isOriginLeft ? 'left' : 'right' ];
+ var yValue = style[ isOriginTop ? 'top' : 'bottom' ];
+ // convert percent to pixels
+ var layoutSize = this.layout.size;
+ var x = xValue.indexOf('%') != -1 ?
+ ( parseFloat( xValue ) / 100 ) * layoutSize.width : parseInt( xValue, 10 );
+ var y = yValue.indexOf('%') != -1 ?
+ ( parseFloat( yValue ) / 100 ) * layoutSize.height : parseInt( yValue, 10 );
+
+ // clean up 'auto' or other non-integer values
+ x = isNaN( x ) ? 0 : x;
+ y = isNaN( y ) ? 0 : y;
+ // remove padding from measurement
+ x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight;
+ y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom;
+
+ this.position.x = x;
+ this.position.y = y;
+};
+
+// set settled position, apply padding
+proto.layoutPosition = function() {
+ var layoutSize = this.layout.size;
+ var style = {};
+ var isOriginLeft = this.layout._getOption('originLeft');
+ var isOriginTop = this.layout._getOption('originTop');
+
+ // x
+ var xPadding = isOriginLeft ? 'paddingLeft' : 'paddingRight';
+ var xProperty = isOriginLeft ? 'left' : 'right';
+ var xResetProperty = isOriginLeft ? 'right' : 'left';
+
+ var x = this.position.x + layoutSize[ xPadding ];
+ // set in percentage or pixels
+ style[ xProperty ] = this.getXValue( x );
+ // reset other property
+ style[ xResetProperty ] = '';
+
+ // y
+ var yPadding = isOriginTop ? 'paddingTop' : 'paddingBottom';
+ var yProperty = isOriginTop ? 'top' : 'bottom';
+ var yResetProperty = isOriginTop ? 'bottom' : 'top';
+
+ var y = this.position.y + layoutSize[ yPadding ];
+ // set in percentage or pixels
+ style[ yProperty ] = this.getYValue( y );
+ // reset other property
+ style[ yResetProperty ] = '';
+
+ this.css( style );
+ this.emitEvent( 'layout', [ this ] );
+};
+
+proto.getXValue = function( x ) {
+ var isHorizontal = this.layout._getOption('horizontal');
+ return this.layout.options.percentPosition && !isHorizontal ?
+ ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px';
+};
+
+proto.getYValue = function( y ) {
+ var isHorizontal = this.layout._getOption('horizontal');
+ return this.layout.options.percentPosition && isHorizontal ?
+ ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px';
+};
+
+proto._transitionTo = function( x, y ) {
+ this.getPosition();
+ // get current x & y from top/left
+ var curX = this.position.x;
+ var curY = this.position.y;
+
+ var compareX = parseInt( x, 10 );
+ var compareY = parseInt( y, 10 );
+ var didNotMove = compareX === this.position.x && compareY === this.position.y;
+
+ // save end position
+ this.setPosition( x, y );
+
+ // if did not move and not transitioning, just go to layout
+ if ( didNotMove && !this.isTransitioning ) {
+ this.layoutPosition();
+ return;
+ }
+
+ var transX = x - curX;
+ var transY = y - curY;
+ var transitionStyle = {};
+ transitionStyle.transform = this.getTranslate( transX, transY );
+
+ this.transition({
+ to: transitionStyle,
+ onTransitionEnd: {
+ transform: this.layoutPosition
+ },
+ isCleaning: true
+ });
+};
+
+proto.getTranslate = function( x, y ) {
+ // flip cooridinates if origin on right or bottom
+ var isOriginLeft = this.layout._getOption('originLeft');
+ var isOriginTop = this.layout._getOption('originTop');
+ x = isOriginLeft ? x : -x;
+ y = isOriginTop ? y : -y;
+ return 'translate3d(' + x + 'px, ' + y + 'px, 0)';
+};
+
+// non transition + transform support
+proto.goTo = function( x, y ) {
+ this.setPosition( x, y );
+ this.layoutPosition();
+};
+
+proto.moveTo = proto._transitionTo;
+
+proto.setPosition = function( x, y ) {
+ this.position.x = parseInt( x, 10 );
+ this.position.y = parseInt( y, 10 );
+};
+
+// ----- transition ----- //
+
+/**
+ * @param {Object} style - CSS
+ * @param {Function} onTransitionEnd
+ */
+
+// non transition, just trigger callback
+proto._nonTransition = function( args ) {
+ this.css( args.to );
+ if ( args.isCleaning ) {
+ this._removeStyles( args.to );
+ }
+ for ( var prop in args.onTransitionEnd ) {
+ args.onTransitionEnd[ prop ].call( this );
+ }
+};
+
+/**
+ * proper transition
+ * @param {Object} args - arguments
+ * @param {Object} to - style to transition to
+ * @param {Object} from - style to start transition from
+ * @param {Boolean} isCleaning - removes transition styles after transition
+ * @param {Function} onTransitionEnd - callback
+ */
+proto.transition = function( args ) {
+ // redirect to nonTransition if no transition duration
+ if ( !parseFloat( this.layout.options.transitionDuration ) ) {
+ this._nonTransition( args );
+ return;
+ }
+
+ var _transition = this._transn;
+ // keep track of onTransitionEnd callback by css property
+ for ( var prop in args.onTransitionEnd ) {
+ _transition.onEnd[ prop ] = args.onTransitionEnd[ prop ];
+ }
+ // keep track of properties that are transitioning
+ for ( prop in args.to ) {
+ _transition.ingProperties[ prop ] = true;
+ // keep track of properties to clean up when transition is done
+ if ( args.isCleaning ) {
+ _transition.clean[ prop ] = true;
+ }
+ }
+
+ // set from styles
+ if ( args.from ) {
+ this.css( args.from );
+ // force redraw. http://blog.alexmaccaw.com/css-transitions
+ var h = this.element.offsetHeight;
+ // hack for JSHint to hush about unused var
+ h = null;
+ }
+ // enable transition
+ this.enableTransition( args.to );
+ // set styles that are transitioning
+ this.css( args.to );
+
+ this.isTransitioning = true;
+
+};
+
+// dash before all cap letters, including first for
+// WebkitTransform => -webkit-transform
+function toDashedAll( str ) {
+ return str.replace( /([A-Z])/g, function( $1 ) {
+ return '-' + $1.toLowerCase();
+ });
+}
+
+var transitionProps = 'opacity,' + toDashedAll( transformProperty );
+
+proto.enableTransition = function(/* style */) {
+ // HACK changing transitionProperty during a transition
+ // will cause transition to jump
+ if ( this.isTransitioning ) {
+ return;
+ }
+
+ // make `transition: foo, bar, baz` from style object
+ // HACK un-comment this when enableTransition can work
+ // while a transition is happening
+ // var transitionValues = [];
+ // for ( var prop in style ) {
+ // // dash-ify camelCased properties like WebkitTransition
+ // prop = vendorProperties[ prop ] || prop;
+ // transitionValues.push( toDashedAll( prop ) );
+ // }
+ // munge number to millisecond, to match stagger
+ var duration = this.layout.options.transitionDuration;
+ duration = typeof duration == 'number' ? duration + 'ms' : duration;
+ // enable transition styles
+ this.css({
+ transitionProperty: transitionProps,
+ transitionDuration: duration,
+ transitionDelay: this.staggerDelay || 0
+ });
+ // listen for transition end event
+ this.element.addEventListener( transitionEndEvent, this, false );
+};
+
+// ----- events ----- //
+
+proto.onwebkitTransitionEnd = function( event ) {
+ this.ontransitionend( event );
+};
+
+proto.onotransitionend = function( event ) {
+ this.ontransitionend( event );
+};
+
+// properties that I munge to make my life easier
+var dashedVendorProperties = {
+ '-webkit-transform': 'transform'
+};
+
+proto.ontransitionend = function( event ) {
+ // disregard bubbled events from children
+ if ( event.target !== this.element ) {
+ return;
+ }
+ var _transition = this._transn;
+ // get property name of transitioned property, convert to prefix-free
+ var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName;
+
+ // remove property that has completed transitioning
+ delete _transition.ingProperties[ propertyName ];
+ // check if any properties are still transitioning
+ if ( isEmptyObj( _transition.ingProperties ) ) {
+ // all properties have completed transitioning
+ this.disableTransition();
+ }
+ // clean style
+ if ( propertyName in _transition.clean ) {
+ // clean up style
+ this.element.style[ event.propertyName ] = '';
+ delete _transition.clean[ propertyName ];
+ }
+ // trigger onTransitionEnd callback
+ if ( propertyName in _transition.onEnd ) {
+ var onTransitionEnd = _transition.onEnd[ propertyName ];
+ onTransitionEnd.call( this );
+ delete _transition.onEnd[ propertyName ];
+ }
+
+ this.emitEvent( 'transitionEnd', [ this ] );
+};
+
+proto.disableTransition = function() {
+ this.removeTransitionStyles();
+ this.element.removeEventListener( transitionEndEvent, this, false );
+ this.isTransitioning = false;
+};
+
+/**
+ * removes style property from element
+ * @param {Object} style
+**/
+proto._removeStyles = function( style ) {
+ // clean up transition styles
+ var cleanStyle = {};
+ for ( var prop in style ) {
+ cleanStyle[ prop ] = '';
+ }
+ this.css( cleanStyle );
+};
+
+var cleanTransitionStyle = {
+ transitionProperty: '',
+ transitionDuration: '',
+ transitionDelay: ''
+};
+
+proto.removeTransitionStyles = function() {
+ // remove transition
+ this.css( cleanTransitionStyle );
+};
+
+// ----- stagger ----- //
+
+proto.stagger = function( delay ) {
+ delay = isNaN( delay ) ? 0 : delay;
+ this.staggerDelay = delay + 'ms';
+};
+
+// ----- show/hide/remove ----- //
+
+// remove element from DOM
+proto.removeElem = function() {
+ this.element.parentNode.removeChild( this.element );
+ // remove display: none
+ this.css({ display: '' });
+ this.emitEvent( 'remove', [ this ] );
+};
+
+proto.remove = function() {
+ // just remove element if no transition support or no transition
+ if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) {
+ this.removeElem();
+ return;
+ }
+
+ // start transition
+ this.once( 'transitionEnd', function() {
+ this.removeElem();
+ });
+ this.hide();
+};
+
+proto.reveal = function() {
+ delete this.isHidden;
+ // remove display: none
+ this.css({ display: '' });
+
+ var options = this.layout.options;
+
+ var onTransitionEnd = {};
+ var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle');
+ onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd;
+
+ this.transition({
+ from: options.hiddenStyle,
+ to: options.visibleStyle,
+ isCleaning: true,
+ onTransitionEnd: onTransitionEnd
+ });
+};
+
+proto.onRevealTransitionEnd = function() {
+ // check if still visible
+ // during transition, item may have been hidden
+ if ( !this.isHidden ) {
+ this.emitEvent('reveal');
+ }
+};
+
+/**
+ * get style property use for hide/reveal transition end
+ * @param {String} styleProperty - hiddenStyle/visibleStyle
+ * @returns {String}
+ */
+proto.getHideRevealTransitionEndProperty = function( styleProperty ) {
+ var optionStyle = this.layout.options[ styleProperty ];
+ // use opacity
+ if ( optionStyle.opacity ) {
+ return 'opacity';
+ }
+ // get first property
+ for ( var prop in optionStyle ) {
+ return prop;
+ }
+};
+
+proto.hide = function() {
+ // set flag
+ this.isHidden = true;
+ // remove display: none
+ this.css({ display: '' });
+
+ var options = this.layout.options;
+
+ var onTransitionEnd = {};
+ var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle');
+ onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd;
+
+ this.transition({
+ from: options.visibleStyle,
+ to: options.hiddenStyle,
+ // keep hidden stuff hidden
+ isCleaning: true,
+ onTransitionEnd: onTransitionEnd
+ });
+};
+
+proto.onHideTransitionEnd = function() {
+ // check if still hidden
+ // during transition, item may have been un-hidden
+ if ( this.isHidden ) {
+ this.css({ display: 'none' });
+ this.emitEvent('hide');
+ }
+};
+
+proto.destroy = function() {
+ this.css({
+ position: '',
+ left: '',
+ right: '',
+ top: '',
+ bottom: '',
+ transition: '',
+ transform: ''
+ });
+};
+
+return Item;
+
+}));
+
+/*!
+ * Outlayer v2.1.0
+ * the brains and guts of a layout library
+ * MIT license
+ */
+
+( function( window, factory ) {
+ 'use strict';
+ // universal module definition
+ /* jshint strict: false */ /* globals define, module, require */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD - RequireJS
+ define( 'outlayer/outlayer',[
+ 'ev-emitter/ev-emitter',
+ 'get-size/get-size',
+ 'fizzy-ui-utils/utils',
+ './item'
+ ],
+ function( EvEmitter, getSize, utils, Item ) {
+ return factory( window, EvEmitter, getSize, utils, Item);
+ }
+ );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS - Browserify, Webpack
+ module.exports = factory(
+ window,
+ require('ev-emitter'),
+ require('get-size'),
+ require('fizzy-ui-utils'),
+ require('./item')
+ );
+ } else {
+ // browser global
+ window.Outlayer = factory(
+ window,
+ window.EvEmitter,
+ window.getSize,
+ window.fizzyUIUtils,
+ window.Outlayer.Item
+ );
+ }
+
+}( window, function factory( window, EvEmitter, getSize, utils, Item ) {
+'use strict';
+
+// ----- vars ----- //
+
+var console = window.console;
+var jQuery = window.jQuery;
+var noop = function() {};
+
+// -------------------------- Outlayer -------------------------- //
+
+// globally unique identifiers
+var GUID = 0;
+// internal store of all Outlayer intances
+var instances = {};
+
+
+/**
+ * @param {Element, String} element
+ * @param {Object} options
+ * @constructor
+ */
+function Outlayer( element, options ) {
+ var queryElement = utils.getQueryElement( element );
+ if ( !queryElement ) {
+ if ( console ) {
+ console.error( 'Bad element for ' + this.constructor.namespace +
+ ': ' + ( queryElement || element ) );
+ }
+ return;
+ }
+ this.element = queryElement;
+ // add jQuery
+ if ( jQuery ) {
+ this.$element = jQuery( this.element );
+ }
+
+ // options
+ this.options = utils.extend( {}, this.constructor.defaults );
+ this.option( options );
+
+ // add id for Outlayer.getFromElement
+ var id = ++GUID;
+ this.element.outlayerGUID = id; // expando
+ instances[ id ] = this; // associate via id
+
+ // kick it off
+ this._create();
+
+ var isInitLayout = this._getOption('initLayout');
+ if ( isInitLayout ) {
+ this.layout();
+ }
+}
+
+// settings are for internal use only
+Outlayer.namespace = 'outlayer';
+Outlayer.Item = Item;
+
+// default options
+Outlayer.defaults = {
+ containerStyle: {
+ position: 'relative'
+ },
+ initLayout: true,
+ originLeft: true,
+ originTop: true,
+ resize: true,
+ resizeContainer: true,
+ // item options
+ transitionDuration: '0.4s',
+ hiddenStyle: {
+ opacity: 0,
+ transform: 'scale(0.001)'
+ },
+ visibleStyle: {
+ opacity: 1,
+ transform: 'scale(1)'
+ }
+};
+
+var proto = Outlayer.prototype;
+// inherit EvEmitter
+utils.extend( proto, EvEmitter.prototype );
+
+/**
+ * set options
+ * @param {Object} opts
+ */
+proto.option = function( opts ) {
+ utils.extend( this.options, opts );
+};
+
+/**
+ * get backwards compatible option value, check old name
+ */
+proto._getOption = function( option ) {
+ var oldOption = this.constructor.compatOptions[ option ];
+ return oldOption && this.options[ oldOption ] !== undefined ?
+ this.options[ oldOption ] : this.options[ option ];
+};
+
+Outlayer.compatOptions = {
+ // currentName: oldName
+ initLayout: 'isInitLayout',
+ horizontal: 'isHorizontal',
+ layoutInstant: 'isLayoutInstant',
+ originLeft: 'isOriginLeft',
+ originTop: 'isOriginTop',
+ resize: 'isResizeBound',
+ resizeContainer: 'isResizingContainer'
+};
+
+proto._create = function() {
+ // get items from children
+ this.reloadItems();
+ // elements that affect layout, but are not laid out
+ this.stamps = [];
+ this.stamp( this.options.stamp );
+ // set container style
+ utils.extend( this.element.style, this.options.containerStyle );
+
+ // bind resize method
+ var canBindResize = this._getOption('resize');
+ if ( canBindResize ) {
+ this.bindResize();
+ }
+};
+
+// goes through all children again and gets bricks in proper order
+proto.reloadItems = function() {
+ // collection of item elements
+ this.items = this._itemize( this.element.children );
+};
+
+
+/**
+ * turn elements into Outlayer.Items to be used in layout
+ * @param {Array or NodeList or HTMLElement} elems
+ * @returns {Array} items - collection of new Outlayer Items
+ */
+proto._itemize = function( elems ) {
+
+ var itemElems = this._filterFindItemElements( elems );
+ var Item = this.constructor.Item;
+
+ // create new Outlayer Items for collection
+ var items = [];
+ for ( var i=0; i < itemElems.length; i++ ) {
+ var elem = itemElems[i];
+ var item = new Item( elem, this );
+ items.push( item );
+ }
+
+ return items;
+};
+
+/**
+ * get item elements to be used in layout
+ * @param {Array or NodeList or HTMLElement} elems
+ * @returns {Array} items - item elements
+ */
+proto._filterFindItemElements = function( elems ) {
+ return utils.filterFindElements( elems, this.options.itemSelector );
+};
+
+/**
+ * getter method for getting item elements
+ * @returns {Array} elems - collection of item elements
+ */
+proto.getItemElements = function() {
+ return this.items.map( function( item ) {
+ return item.element;
+ });
+};
+
+// ----- init & layout ----- //
+
+/**
+ * lays out all items
+ */
+proto.layout = function() {
+ this._resetLayout();
+ this._manageStamps();
+
+ // don't animate first layout
+ var layoutInstant = this._getOption('layoutInstant');
+ var isInstant = layoutInstant !== undefined ?
+ layoutInstant : !this._isLayoutInited;
+ this.layoutItems( this.items, isInstant );
+
+ // flag for initalized
+ this._isLayoutInited = true;
+};
+
+// _init is alias for layout
+proto._init = proto.layout;
+
+/**
+ * logic before any new layout
+ */
+proto._resetLayout = function() {
+ this.getSize();
+};
+
+
+proto.getSize = function() {
+ this.size = getSize( this.element );
+};
+
+/**
+ * get measurement from option, for columnWidth, rowHeight, gutter
+ * if option is String -> get element from selector string, & get size of element
+ * if option is Element -> get size of element
+ * else use option as a number
+ *
+ * @param {String} measurement
+ * @param {String} size - width or height
+ * @private
+ */
+proto._getMeasurement = function( measurement, size ) {
+ var option = this.options[ measurement ];
+ var elem;
+ if ( !option ) {
+ // default to 0
+ this[ measurement ] = 0;
+ } else {
+ // use option as an element
+ if ( typeof option == 'string' ) {
+ elem = this.element.querySelector( option );
+ } else if ( option instanceof HTMLElement ) {
+ elem = option;
+ }
+ // use size of element, if element
+ this[ measurement ] = elem ? getSize( elem )[ size ] : option;
+ }
+};
+
+/**
+ * layout a collection of item elements
+ * @api public
+ */
+proto.layoutItems = function( items, isInstant ) {
+ items = this._getItemsForLayout( items );
+
+ this._layoutItems( items, isInstant );
+
+ this._postLayout();
+};
+
+/**
+ * get the items to be laid out
+ * you may want to skip over some items
+ * @param {Array} items
+ * @returns {Array} items
+ */
+proto._getItemsForLayout = function( items ) {
+ return items.filter( function( item ) {
+ return !item.isIgnored;
+ });
+};
+
+/**
+ * layout items
+ * @param {Array} items
+ * @param {Boolean} isInstant
+ */
+proto._layoutItems = function( items, isInstant ) {
+ this._emitCompleteOnItems( 'layout', items );
+
+ if ( !items || !items.length ) {
+ // no items, emit event with empty array
+ return;
+ }
+
+ var queue = [];
+
+ items.forEach( function( item ) {
+ // get x/y object from method
+ var position = this._getItemLayoutPosition( item );
+ // enqueue
+ position.item = item;
+ position.isInstant = isInstant || item.isLayoutInstant;
+ queue.push( position );
+ }, this );
+
+ this._processLayoutQueue( queue );
+};
+
+/**
+ * get item layout position
+ * @param {Outlayer.Item} item
+ * @returns {Object} x and y position
+ */
+proto._getItemLayoutPosition = function( /* item */ ) {
+ return {
+ x: 0,
+ y: 0
+ };
+};
+
+/**
+ * iterate over array and position each item
+ * Reason being - separating this logic prevents 'layout invalidation'
+ * thx @paul_irish
+ * @param {Array} queue
+ */
+proto._processLayoutQueue = function( queue ) {
+ this.updateStagger();
+ queue.forEach( function( obj, i ) {
+ this._positionItem( obj.item, obj.x, obj.y, obj.isInstant, i );
+ }, this );
+};
+
+// set stagger from option in milliseconds number
+proto.updateStagger = function() {
+ var stagger = this.options.stagger;
+ if ( stagger === null || stagger === undefined ) {
+ this.stagger = 0;
+ return;
+ }
+ this.stagger = getMilliseconds( stagger );
+ return this.stagger;
+};
+
+/**
+ * Sets position of item in DOM
+ * @param {Outlayer.Item} item
+ * @param {Number} x - horizontal position
+ * @param {Number} y - vertical position
+ * @param {Boolean} isInstant - disables transitions
+ */
+proto._positionItem = function( item, x, y, isInstant, i ) {
+ if ( isInstant ) {
+ // if not transition, just set CSS
+ item.goTo( x, y );
+ } else {
+ item.stagger( i * this.stagger );
+ item.moveTo( x, y );
+ }
+};
+
+/**
+ * Any logic you want to do after each layout,
+ * i.e. size the container
+ */
+proto._postLayout = function() {
+ this.resizeContainer();
+};
+
+proto.resizeContainer = function() {
+ var isResizingContainer = this._getOption('resizeContainer');
+ if ( !isResizingContainer ) {
+ return;
+ }
+ var size = this._getContainerSize();
+ if ( size ) {
+ this._setContainerMeasure( size.width, true );
+ this._setContainerMeasure( size.height, false );
+ }
+};
+
+/**
+ * Sets width or height of container if returned
+ * @returns {Object} size
+ * @param {Number} width
+ * @param {Number} height
+ */
+proto._getContainerSize = noop;
+
+/**
+ * @param {Number} measure - size of width or height
+ * @param {Boolean} isWidth
+ */
+proto._setContainerMeasure = function( measure, isWidth ) {
+ if ( measure === undefined ) {
+ return;
+ }
+
+ var elemSize = this.size;
+ // add padding and border width if border box
+ if ( elemSize.isBorderBox ) {
+ measure += isWidth ? elemSize.paddingLeft + elemSize.paddingRight +
+ elemSize.borderLeftWidth + elemSize.borderRightWidth :
+ elemSize.paddingBottom + elemSize.paddingTop +
+ elemSize.borderTopWidth + elemSize.borderBottomWidth;
+ }
+
+ measure = Math.max( measure, 0 );
+ this.element.style[ isWidth ? 'width' : 'height' ] = measure + 'px';
+};
+
+/**
+ * emit eventComplete on a collection of items events
+ * @param {String} eventName
+ * @param {Array} items - Outlayer.Items
+ */
+proto._emitCompleteOnItems = function( eventName, items ) {
+ var _this = this;
+ function onComplete() {
+ _this.dispatchEvent( eventName + 'Complete', null, [ items ] );
+ }
+
+ var count = items.length;
+ if ( !items || !count ) {
+ onComplete();
+ return;
+ }
+
+ var doneCount = 0;
+ function tick() {
+ doneCount++;
+ if ( doneCount == count ) {
+ onComplete();
+ }
+ }
+
+ // bind callback
+ items.forEach( function( item ) {
+ item.once( eventName, tick );
+ });
+};
+
+/**
+ * emits events via EvEmitter and jQuery events
+ * @param {String} type - name of event
+ * @param {Event} event - original event
+ * @param {Array} args - extra arguments
+ */
+proto.dispatchEvent = function( type, event, args ) {
+ // add original event to arguments
+ var emitArgs = event ? [ event ].concat( args ) : args;
+ this.emitEvent( type, emitArgs );
+
+ if ( jQuery ) {
+ // set this.$element
+ this.$element = this.$element || jQuery( this.element );
+ if ( event ) {
+ // create jQuery event
+ var $event = jQuery.Event( event );
+ $event.type = type;
+ this.$element.trigger( $event, args );
+ } else {
+ // just trigger with type if no event available
+ this.$element.trigger( type, args );
+ }
+ }
+};
+
+// -------------------------- ignore & stamps -------------------------- //
+
+
+/**
+ * keep item in collection, but do not lay it out
+ * ignored items do not get skipped in layout
+ * @param {Element} elem
+ */
+proto.ignore = function( elem ) {
+ var item = this.getItem( elem );
+ if ( item ) {
+ item.isIgnored = true;
+ }
+};
+
+/**
+ * return item to layout collection
+ * @param {Element} elem
+ */
+proto.unignore = function( elem ) {
+ var item = this.getItem( elem );
+ if ( item ) {
+ delete item.isIgnored;
+ }
+};
+
+/**
+ * adds elements to stamps
+ * @param {NodeList, Array, Element, or String} elems
+ */
+proto.stamp = function( elems ) {
+ elems = this._find( elems );
+ if ( !elems ) {
+ return;
+ }
+
+ this.stamps = this.stamps.concat( elems );
+ // ignore
+ elems.forEach( this.ignore, this );
+};
+
+/**
+ * removes elements to stamps
+ * @param {NodeList, Array, or Element} elems
+ */
+proto.unstamp = function( elems ) {
+ elems = this._find( elems );
+ if ( !elems ){
+ return;
+ }
+
+ elems.forEach( function( elem ) {
+ // filter out removed stamp elements
+ utils.removeFrom( this.stamps, elem );
+ this.unignore( elem );
+ }, this );
+};
+
+/**
+ * finds child elements
+ * @param {NodeList, Array, Element, or String} elems
+ * @returns {Array} elems
+ */
+proto._find = function( elems ) {
+ if ( !elems ) {
+ return;
+ }
+ // if string, use argument as selector string
+ if ( typeof elems == 'string' ) {
+ elems = this.element.querySelectorAll( elems );
+ }
+ elems = utils.makeArray( elems );
+ return elems;
+};
+
+proto._manageStamps = function() {
+ if ( !this.stamps || !this.stamps.length ) {
+ return;
+ }
+
+ this._getBoundingRect();
+
+ this.stamps.forEach( this._manageStamp, this );
+};
+
+// update boundingLeft / Top
+proto._getBoundingRect = function() {
+ // get bounding rect for container element
+ var boundingRect = this.element.getBoundingClientRect();
+ var size = this.size;
+ this._boundingRect = {
+ left: boundingRect.left + size.paddingLeft + size.borderLeftWidth,
+ top: boundingRect.top + size.paddingTop + size.borderTopWidth,
+ right: boundingRect.right - ( size.paddingRight + size.borderRightWidth ),
+ bottom: boundingRect.bottom - ( size.paddingBottom + size.borderBottomWidth )
+ };
+};
+
+/**
+ * @param {Element} stamp
+**/
+proto._manageStamp = noop;
+
+/**
+ * get x/y position of element relative to container element
+ * @param {Element} elem
+ * @returns {Object} offset - has left, top, right, bottom
+ */
+proto._getElementOffset = function( elem ) {
+ var boundingRect = elem.getBoundingClientRect();
+ var thisRect = this._boundingRect;
+ var size = getSize( elem );
+ var offset = {
+ left: boundingRect.left - thisRect.left - size.marginLeft,
+ top: boundingRect.top - thisRect.top - size.marginTop,
+ right: thisRect.right - boundingRect.right - size.marginRight,
+ bottom: thisRect.bottom - boundingRect.bottom - size.marginBottom
+ };
+ return offset;
+};
+
+// -------------------------- resize -------------------------- //
+
+// enable event handlers for listeners
+// i.e. resize -> onresize
+proto.handleEvent = utils.handleEvent;
+
+/**
+ * Bind layout to window resizing
+ */
+proto.bindResize = function() {
+ window.addEventListener( 'resize', this );
+ this.isResizeBound = true;
+};
+
+/**
+ * Unbind layout to window resizing
+ */
+proto.unbindResize = function() {
+ window.removeEventListener( 'resize', this );
+ this.isResizeBound = false;
+};
+
+proto.onresize = function() {
+ this.resize();
+};
+
+utils.debounceMethod( Outlayer, 'onresize', 100 );
+
+proto.resize = function() {
+ // don't trigger if size did not change
+ // or if resize was unbound. See #9
+ if ( !this.isResizeBound || !this.needsResizeLayout() ) {
+ return;
+ }
+
+ this.layout();
+};
+
+/**
+ * check if layout is needed post layout
+ * @returns Boolean
+ */
+proto.needsResizeLayout = function() {
+ var size = getSize( this.element );
+ // check that this.size and size are there
+ // IE8 triggers resize on body size change, so they might not be
+ var hasSizes = this.size && size;
+ return hasSizes && size.innerWidth !== this.size.innerWidth;
+};
+
+// -------------------------- methods -------------------------- //
+
+/**
+ * add items to Outlayer instance
+ * @param {Array or NodeList or Element} elems
+ * @returns {Array} items - Outlayer.Items
+**/
+proto.addItems = function( elems ) {
+ var items = this._itemize( elems );
+ // add items to collection
+ if ( items.length ) {
+ this.items = this.items.concat( items );
+ }
+ return items;
+};
+
+/**
+ * Layout newly-appended item elements
+ * @param {Array or NodeList or Element} elems
+ */
+proto.appended = function( elems ) {
+ var items = this.addItems( elems );
+ if ( !items.length ) {
+ return;
+ }
+ // layout and reveal just the new items
+ this.layoutItems( items, true );
+ this.reveal( items );
+};
+
+/**
+ * Layout prepended elements
+ * @param {Array or NodeList or Element} elems
+ */
+proto.prepended = function( elems ) {
+ var items = this._itemize( elems );
+ if ( !items.length ) {
+ return;
+ }
+ // add items to beginning of collection
+ var previousItems = this.items.slice(0);
+ this.items = items.concat( previousItems );
+ // start new layout
+ this._resetLayout();
+ this._manageStamps();
+ // layout new stuff without transition
+ this.layoutItems( items, true );
+ this.reveal( items );
+ // layout previous items
+ this.layoutItems( previousItems );
+};
+
+/**
+ * reveal a collection of items
+ * @param {Array of Outlayer.Items} items
+ */
+proto.reveal = function( items ) {
+ this._emitCompleteOnItems( 'reveal', items );
+ if ( !items || !items.length ) {
+ return;
+ }
+ var stagger = this.updateStagger();
+ items.forEach( function( item, i ) {
+ item.stagger( i * stagger );
+ item.reveal();
+ });
+};
+
+/**
+ * hide a collection of items
+ * @param {Array of Outlayer.Items} items
+ */
+proto.hide = function( items ) {
+ this._emitCompleteOnItems( 'hide', items );
+ if ( !items || !items.length ) {
+ return;
+ }
+ var stagger = this.updateStagger();
+ items.forEach( function( item, i ) {
+ item.stagger( i * stagger );
+ item.hide();
+ });
+};
+
+/**
+ * reveal item elements
+ * @param {Array}, {Element}, {NodeList} items
+ */
+proto.revealItemElements = function( elems ) {
+ var items = this.getItems( elems );
+ this.reveal( items );
+};
+
+/**
+ * hide item elements
+ * @param {Array}, {Element}, {NodeList} items
+ */
+proto.hideItemElements = function( elems ) {
+ var items = this.getItems( elems );
+ this.hide( items );
+};
+
+/**
+ * get Outlayer.Item, given an Element
+ * @param {Element} elem
+ * @param {Function} callback
+ * @returns {Outlayer.Item} item
+ */
+proto.getItem = function( elem ) {
+ // loop through items to get the one that matches
+ for ( var i=0; i < this.items.length; i++ ) {
+ var item = this.items[i];
+ if ( item.element == elem ) {
+ // return item
+ return item;
+ }
+ }
+};
+
+/**
+ * get collection of Outlayer.Items, given Elements
+ * @param {Array} elems
+ * @returns {Array} items - Outlayer.Items
+ */
+proto.getItems = function( elems ) {
+ elems = utils.makeArray( elems );
+ var items = [];
+ elems.forEach( function( elem ) {
+ var item = this.getItem( elem );
+ if ( item ) {
+ items.push( item );
+ }
+ }, this );
+
+ return items;
+};
+
+/**
+ * remove element(s) from instance and DOM
+ * @param {Array or NodeList or Element} elems
+ */
+proto.remove = function( elems ) {
+ var removeItems = this.getItems( elems );
+
+ this._emitCompleteOnItems( 'remove', removeItems );
+
+ // bail if no items to remove
+ if ( !removeItems || !removeItems.length ) {
+ return;
+ }
+
+ removeItems.forEach( function( item ) {
+ item.remove();
+ // remove item from collection
+ utils.removeFrom( this.items, item );
+ }, this );
+};
+
+// ----- destroy ----- //
+
+// remove and disable Outlayer instance
+proto.destroy = function() {
+ // clean up dynamic styles
+ var style = this.element.style;
+ style.height = '';
+ style.position = '';
+ style.width = '';
+ // destroy items
+ this.items.forEach( function( item ) {
+ item.destroy();
+ });
+
+ this.unbindResize();
+
+ var id = this.element.outlayerGUID;
+ delete instances[ id ]; // remove reference to instance by id
+ delete this.element.outlayerGUID;
+ // remove data for jQuery
+ if ( jQuery ) {
+ jQuery.removeData( this.element, this.constructor.namespace );
+ }
+
+};
+
+// -------------------------- data -------------------------- //
+
+/**
+ * get Outlayer instance from element
+ * @param {Element} elem
+ * @returns {Outlayer}
+ */
+Outlayer.data = function( elem ) {
+ elem = utils.getQueryElement( elem );
+ var id = elem && elem.outlayerGUID;
+ return id && instances[ id ];
+};
+
+
+// -------------------------- create Outlayer class -------------------------- //
+
+/**
+ * create a layout class
+ * @param {String} namespace
+ */
+Outlayer.create = function( namespace, options ) {
+ // sub-class Outlayer
+ var Layout = subclass( Outlayer );
+ // apply new options and compatOptions
+ Layout.defaults = utils.extend( {}, Outlayer.defaults );
+ utils.extend( Layout.defaults, options );
+ Layout.compatOptions = utils.extend( {}, Outlayer.compatOptions );
+
+ Layout.namespace = namespace;
+
+ Layout.data = Outlayer.data;
+
+ // sub-class Item
+ Layout.Item = subclass( Item );
+
+ // -------------------------- declarative -------------------------- //
+
+ utils.htmlInit( Layout, namespace );
+
+ // -------------------------- jQuery bridge -------------------------- //
+
+ // make into jQuery plugin
+ if ( jQuery && jQuery.bridget ) {
+ jQuery.bridget( namespace, Layout );
+ }
+
+ return Layout;
+};
+
+function subclass( Parent ) {
+ function SubClass() {
+ Parent.apply( this, arguments );
+ }
+
+ SubClass.prototype = Object.create( Parent.prototype );
+ SubClass.prototype.constructor = SubClass;
+
+ return SubClass;
+}
+
+// ----- helpers ----- //
+
+// how many milliseconds are in each unit
+var msUnits = {
+ ms: 1,
+ s: 1000
+};
+
+// munge time-like parameter into millisecond number
+// '0.4s' -> 40
+function getMilliseconds( time ) {
+ if ( typeof time == 'number' ) {
+ return time;
+ }
+ var matches = time.match( /(^\d*\.?\d*)(\w*)/ );
+ var num = matches && matches[1];
+ var unit = matches && matches[2];
+ if ( !num.length ) {
+ return 0;
+ }
+ num = parseFloat( num );
+ var mult = msUnits[ unit ] || 1;
+ return num * mult;
+}
+
+// ----- fin ----- //
+
+// back in global
+Outlayer.Item = Item;
+
+return Outlayer;
+
+}));
+
+/*!
+ * Masonry v4.2.1
+ * Cascading grid layout library
+ * https://masonry.desandro.com
+ * MIT License
+ * by David DeSandro
+ */
+
+( function( window, factory ) {
+ // universal module definition
+ /* jshint strict: false */ /*globals define, module, require */
+ if ( typeof define == 'function' && define.amd ) {
+ // AMD
+ define( [
+ 'outlayer/outlayer',
+ 'get-size/get-size'
+ ],
+ factory );
+ } else if ( typeof module == 'object' && module.exports ) {
+ // CommonJS
+ module.exports = factory(
+ require('outlayer'),
+ require('get-size')
+ );
+ } else {
+ // browser global
+ window.Masonry = factory(
+ window.Outlayer,
+ window.getSize
+ );
+ }
+
+}( window, function factory( Outlayer, getSize ) {
+
+
+
+// -------------------------- masonryDefinition -------------------------- //
+
+ // create an Outlayer layout class
+ var Masonry = Outlayer.create('masonry');
+ // isFitWidth -> fitWidth
+ Masonry.compatOptions.fitWidth = 'isFitWidth';
+
+ var proto = Masonry.prototype;
+
+ proto._resetLayout = function() {
+ this.getSize();
+ this._getMeasurement( 'columnWidth', 'outerWidth' );
+ this._getMeasurement( 'gutter', 'outerWidth' );
+ this.measureColumns();
+
+ // reset column Y
+ this.colYs = [];
+ for ( var i=0; i < this.cols; i++ ) {
+ this.colYs.push( 0 );
+ }
+
+ this.maxY = 0;
+ this.horizontalColIndex = 0;
+ };
+
+ proto.measureColumns = function() {
+ this.getContainerWidth();
+ // if columnWidth is 0, default to outerWidth of first item
+ if ( !this.columnWidth ) {
+ var firstItem = this.items[0];
+ var firstItemElem = firstItem && firstItem.element;
+ // columnWidth fall back to item of first element
+ this.columnWidth = firstItemElem && getSize( firstItemElem ).outerWidth ||
+ // if first elem has no width, default to size of container
+ this.containerWidth;
+ }
+
+ var columnWidth = this.columnWidth += this.gutter;
+
+ // calculate columns
+ var containerWidth = this.containerWidth + this.gutter;
+ var cols = containerWidth / columnWidth;
+ // fix rounding errors, typically with gutters
+ var excess = columnWidth - containerWidth % columnWidth;
+ // if overshoot is less than a pixel, round up, otherwise floor it
+ var mathMethod = excess && excess < 1 ? 'round' : 'floor';
+ cols = Math[ mathMethod ]( cols );
+ this.cols = Math.max( cols, 1 );
+ };
+
+ proto.getContainerWidth = function() {
+ // container is parent if fit width
+ var isFitWidth = this._getOption('fitWidth');
+ var container = isFitWidth ? this.element.parentNode : this.element;
+ // check that this.size and size are there
+ // IE8 triggers resize on body size change, so they might not be
+ var size = getSize( container );
+ this.containerWidth = size && size.innerWidth;
+ };
+
+ proto._getItemLayoutPosition = function( item ) {
+ item.getSize();
+ // how many columns does this brick span
+ var remainder = item.size.outerWidth % this.columnWidth;
+ var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil';
+ // round if off by 1 pixel, otherwise use ceil
+ var colSpan = Math[ mathMethod ]( item.size.outerWidth / this.columnWidth );
+ colSpan = Math.min( colSpan, this.cols );
+ // use horizontal or top column position
+ var colPosMethod = this.options.horizontalOrder ?
+ '_getHorizontalColPosition' : '_getTopColPosition';
+ var colPosition = this[ colPosMethod ]( colSpan, item );
+ // position the brick
+ var position = {
+ x: this.columnWidth * colPosition.col,
+ y: colPosition.y
+ };
+ // apply setHeight to necessary columns
+ var setHeight = colPosition.y + item.size.outerHeight;
+ var setMax = colSpan + colPosition.col;
+ for ( var i = colPosition.col; i < setMax; i++ ) {
+ this.colYs[i] = setHeight;
+ }
+
+ return position;
+ };
+
+ proto._getTopColPosition = function( colSpan ) {
+ var colGroup = this._getTopColGroup( colSpan );
+ // get the minimum Y value from the columns
+ var minimumY = Math.min.apply( Math, colGroup );
+
+ return {
+ col: colGroup.indexOf( minimumY ),
+ y: minimumY,
+ };
+ };
+
+ /**
+ * @param {Number} colSpan - number of columns the element spans
+ * @returns {Array} colGroup
+ */
+ proto._getTopColGroup = function( colSpan ) {
+ if ( colSpan < 2 ) {
+ // if brick spans only one column, use all the column Ys
+ return this.colYs;
+ }
+
+ var colGroup = [];
+ // how many different places could this brick fit horizontally
+ var groupCount = this.cols + 1 - colSpan;
+ // for each group potential horizontal position
+ for ( var i = 0; i < groupCount; i++ ) {
+ colGroup[i] = this._getColGroupY( i, colSpan );
+ }
+ return colGroup;
+ };
+
+ proto._getColGroupY = function( col, colSpan ) {
+ if ( colSpan < 2 ) {
+ return this.colYs[ col ];
+ }
+ // make an array of colY values for that one group
+ var groupColYs = this.colYs.slice( col, col + colSpan );
+ // and get the max value of the array
+ return Math.max.apply( Math, groupColYs );
+ };
+
+ // get column position based on horizontal index. #873
+ proto._getHorizontalColPosition = function( colSpan, item ) {
+ var col = this.horizontalColIndex % this.cols;
+ var isOver = colSpan > 1 && col + colSpan > this.cols;
+ // shift to next row if item can't fit on current row
+ col = isOver ? 0 : col;
+ // don't let zero-size items take up space
+ var hasSize = item.size.outerWidth && item.size.outerHeight;
+ this.horizontalColIndex = hasSize ? col + colSpan : this.horizontalColIndex;
+
+ return {
+ col: col,
+ y: this._getColGroupY( col, colSpan ),
+ };
+ };
+
+ proto._manageStamp = function( stamp ) {
+ var stampSize = getSize( stamp );
+ var offset = this._getElementOffset( stamp );
+ // get the columns that this stamp affects
+ var isOriginLeft = this._getOption('originLeft');
+ var firstX = isOriginLeft ? offset.left : offset.right;
+ var lastX = firstX + stampSize.outerWidth;
+ var firstCol = Math.floor( firstX / this.columnWidth );
+ firstCol = Math.max( 0, firstCol );
+ var lastCol = Math.floor( lastX / this.columnWidth );
+ // lastCol should not go over if multiple of columnWidth #425
+ lastCol -= lastX % this.columnWidth ? 0 : 1;
+ lastCol = Math.min( this.cols - 1, lastCol );
+ // set colYs to bottom of the stamp
+
+ var isOriginTop = this._getOption('originTop');
+ var stampMaxY = ( isOriginTop ? offset.top : offset.bottom ) +
+ stampSize.outerHeight;
+ for ( var i = firstCol; i <= lastCol; i++ ) {
+ this.colYs[i] = Math.max( stampMaxY, this.colYs[i] );
+ }
+ };
+
+ proto._getContainerSize = function() {
+ this.maxY = Math.max.apply( Math, this.colYs );
+ var size = {
+ height: this.maxY
+ };
+
+ if ( this._getOption('fitWidth') ) {
+ size.width = this._getContainerFitWidth();
+ }
+
+ return size;
+ };
+
+ proto._getContainerFitWidth = function() {
+ var unusedCols = 0;
+ // count unused columns
+ var i = this.cols;
+ while ( --i ) {
+ if ( this.colYs[i] !== 0 ) {
+ break;
+ }
+ unusedCols++;
+ }
+ // fit container to columns that have been used
+ return ( this.cols - unusedCols ) * this.columnWidth - this.gutter;
+ };
+
+ proto.needsResizeLayout = function() {
+ var previousWidth = this.containerWidth;
+ this.getContainerWidth();
+ return previousWidth != this.containerWidth;
+ };
+
+ return Masonry;
+
+}));
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.masonry.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.masonry.min.js
new file mode 100644
index 00000000..38fff87c
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.masonry.min.js
@@ -0,0 +1,114 @@
+(function(window,factory){if(typeof define=='function'&&define.amd){define('jquery-bridget/jquery-bridget',['jquery'],function(jQuery){return factory(window,jQuery);});}else if(typeof module=='object'&&module.exports){module.exports=factory(window,require('jquery'));}else{window.jQueryBridget=factory(window,window.jQuery);}}(window,function factory(window,jQuery){'use strict';var arraySlice=Array.prototype.slice;var console=window.console;var logError=typeof console=='undefined'?function(){}:function(message){console.error(message);};function jQueryBridget(namespace,PluginClass,$){$=$||jQuery||window.jQuery;if(!$){return;}
+if(!PluginClass.prototype.option){PluginClass.prototype.option=function(opts){if(!$.isPlainObject(opts)){return;}
+this.options=$.extend(true,this.options,opts);};}
+$.fn[namespace]=function(arg0){if(typeof arg0=='string'){var args=arraySlice.call(arguments,1);return methodCall(this,arg0,args);}
+plainCall(this,arg0);return this;};function methodCall($elems,methodName,args){var returnValue;var pluginMethodStr='$().'+namespace+'("'+methodName+'")';$elems.each(function(i,elem){var instance=$.data(elem,namespace);if(!instance){logError(namespace+' not initialized. Cannot call methods, i.e. '+
+pluginMethodStr);return;}
+var method=instance[methodName];if(!method||methodName.charAt(0)=='_'){logError(pluginMethodStr+' is not a valid method');return;}
+var value=method.apply(instance,args);returnValue=returnValue===undefined?value:returnValue;});return returnValue!==undefined?returnValue:$elems;}
+function plainCall($elems,options){$elems.each(function(i,elem){var instance=$.data(elem,namespace);if(instance){instance.option(options);instance._init();}else{instance=new PluginClass(elem,options);$.data(elem,namespace,instance);}});}
+updateJQuery($);}
+function updateJQuery($){if(!$||($&&$.bridget)){return;}
+$.bridget=jQueryBridget;}
+updateJQuery(jQuery||window.jQuery);return jQueryBridget;}));(function(global,factory){if(typeof define=='function'&&define.amd){define('ev-emitter/ev-emitter',factory);}else if(typeof module=='object'&&module.exports){module.exports=factory();}else{global.EvEmitter=factory();}}(typeof window!='undefined'?window:this,function(){function EvEmitter(){}
+var proto=EvEmitter.prototype;proto.on=function(eventName,listener){if(!eventName||!listener){return;}
+var events=this._events=this._events||{};var listeners=events[eventName]=events[eventName]||[];if(listeners.indexOf(listener)==-1){listeners.push(listener);}
+return this;};proto.once=function(eventName,listener){if(!eventName||!listener){return;}
+this.on(eventName,listener);var onceEvents=this._onceEvents=this._onceEvents||{};var onceListeners=onceEvents[eventName]=onceEvents[eventName]||{};onceListeners[listener]=true;return this;};proto.off=function(eventName,listener){var listeners=this._events&&this._events[eventName];if(!listeners||!listeners.length){return;}
+var index=listeners.indexOf(listener);if(index!=-1){listeners.splice(index,1);}
+return this;};proto.emitEvent=function(eventName,args){var listeners=this._events&&this._events[eventName];if(!listeners||!listeners.length){return;}
+listeners=listeners.slice(0);args=args||[];var onceListeners=this._onceEvents&&this._onceEvents[eventName];for(var i=0;i1&&col+colSpan>this.cols;col=isOver?0:col;var hasSize=item.size.outerWidth&&item.size.outerHeight;this.horizontalColIndex=hasSize?col+colSpan:this.horizontalColIndex;return{col:col,y:this._getColGroupY(col,colSpan),};};proto._manageStamp=function(stamp){var stampSize=getSize(stamp);var offset=this._getElementOffset(stamp);var isOriginLeft=this._getOption('originLeft');var firstX=isOriginLeft?offset.left:offset.right;var lastX=firstX+stampSize.outerWidth;var firstCol=Math.floor(firstX/this.columnWidth);firstCol=Math.max(0,firstCol);var lastCol=Math.floor(lastX/this.columnWidth);lastCol-=lastX%this.columnWidth?0:1;lastCol=Math.min(this.cols-1,lastCol);var isOriginTop=this._getOption('originTop');var stampMaxY=(isOriginTop?offset.top:offset.bottom)+
+stampSize.outerHeight;for(var i=firstCol;i<=lastCol;i++){this.colYs[i]=Math.max(stampMaxY,this.colYs[i]);}};proto._getContainerSize=function(){this.maxY=Math.max.apply(Math,this.colYs);var size={height:this.maxY};if(this._getOption('fitWidth')){size.width=this._getContainerFitWidth();}
+return size;};proto._getContainerFitWidth=function(){var unusedCols=0;var i=this.cols;while(--i){if(this.colYs[i]!==0){break;}
+unusedCols++;}
+return(this.cols-unusedCols)*this.columnWidth-this.gutter;};proto.needsResizeLayout=function(){var previousWidth=this.containerWidth;this.getContainerWidth();return previousWidth!=this.containerWidth;};return Masonry;}));
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.proxy-all.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.proxy-all.js
new file mode 100644
index 00000000..f9b3bbf1
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.proxy-all.js
@@ -0,0 +1,47 @@
+(function (jQuery) {
+ /* Works in a similar fashion to underscore's _.bindAll() but also accepts
+ * regular expressions for method names.
+ *
+ * obj - An object to proxy methods.
+ * args... - Successive method names or regular expressions to bind.
+ *
+ * Examples
+ *
+ * var obj = {
+ * _onClick: function () {}
+ * _onSave: function () {}
+ * };
+ *
+ * // Provide method names to proxy/bind to obj scope.
+ * jQuery.bindAll(obj, '_onClick', '_onSave');
+ *
+ * // Use a RegExp to match patterns.
+ * jQuery.bindAll(obj, /^_on/);
+ *
+ * Returns the original object.
+ */
+ jQuery.proxyAll = function (obj /*, args... */) {
+ var methods = [].slice.call(arguments, 1);
+ var index = 0;
+ var length = methods.length;
+ var property;
+ var method;
+
+ for (; index < length; index += 1) {
+ method = methods[index];
+
+ for (property in obj) {
+ if (typeof obj[property] === 'function') {
+ if ((method instanceof RegExp && method.test(property)) || property === method) {
+ if (obj[property].proxied !== true) {
+ obj[property] = jQuery.proxy(obj[property], obj);
+ obj[property].proxied = true;
+ }
+ }
+ }
+ }
+ }
+
+ return obj;
+ };
+})(this.jQuery);
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.proxy-all.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.proxy-all.min.js
new file mode 100644
index 00000000..f5e77e2e
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.proxy-all.min.js
@@ -0,0 +1,2 @@
+(function(jQuery){jQuery.proxyAll=function(obj){var methods=[].slice.call(arguments,1);var index=0;var length=methods.length;var property;var method;for(;index',
+ * i18n: {edit: 'éditer'}
+ * });
+ * // previews === preview objects.
+ * // previews.end() === [name=slug] objects.
+ *
+ * Returns the newly created collection of preview elements..
+ */
+(function ($, window) {
+ var escape = $.url.escape;
+
+ function slugPreview(options) {
+ options = $.extend(true, slugPreview.defaults, options || {});
+
+ var collected = this.map(function () {
+ var element = $(this);
+ var field = element.find('input');
+ var preview = $(options.template);
+ var value = preview.find('.slug-preview-value');
+ var required = $('').append($('.control-required', element).clone()).html();
+
+ function setValue() {
+ var val = escape(field.val()) || options.placeholder;
+ value.text(val);
+ }
+
+ preview.find('strong').html(required + ' ' + options.i18n['URL'] + ':');
+ preview.find('.slug-preview-prefix').text(options.prefix);
+ preview.find('button').text(options.i18n['Edit']).click(function (event) {
+ event.preventDefault();
+ element.show();
+ preview.hide();
+ });
+
+ setValue();
+ field.on('change', setValue);
+
+ element.after(preview).hide();
+
+ return preview[0];
+ });
+
+ // Append the new elements to the current jQuery stack so that the caller
+ // can modify the elements. Then restore the originals by calling .end().
+ return this.pushStack(collected);
+ }
+
+ slugPreview.defaults = {
+ prefix: '',
+ placeholder: '',
+ i18n: {
+ 'URL': 'URL',
+ 'Edit': 'Edit'
+ },
+ template: [
+ '
',
+ ' ',
+ ' ',
+ ' ',
+ '
'
+ ].join('\n')
+ };
+
+ $.fn.slugPreview = slugPreview;
+
+})(this.jQuery, this);
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug-preview.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug-preview.min.js
new file mode 100644
index 00000000..e92c6231
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug-preview.min.js
@@ -0,0 +1,3 @@
+(function($,window){var escape=$.url.escape;function slugPreview(options){options=$.extend(true,slugPreview.defaults,options||{});var collected=this.map(function(){var element=$(this);var field=element.find('input');var preview=$(options.template);var value=preview.find('.slug-preview-value');var required=$('
').append($('.control-required',element).clone()).html();function setValue(){var val=escape(field.val())||options.placeholder;value.text(val);}
+preview.find('strong').html(required+' '+options.i18n['URL']+':');preview.find('.slug-preview-prefix').text(options.prefix);preview.find('button').text(options.i18n['Edit']).click(function(event){event.preventDefault();element.show();preview.hide();});setValue();field.on('change',setValue);element.after(preview).hide();return preview[0];});return this.pushStack(collected);}
+slugPreview.defaults={prefix:'',placeholder:'',i18n:{'URL':'URL','Edit':'Edit'},template:['
',' ',' ',' ','
'].join('\n')};$.fn.slugPreview=slugPreview;})(this.jQuery,this);
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug.js
new file mode 100644
index 00000000..22e17b18
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug.js
@@ -0,0 +1,83 @@
+/* Restricts the input into the field to just slug safe characters.
+ *
+ * The element will also fire the "slugify" event passing in the new and
+ * previous strings as arguments.
+ *
+ * Examples
+ *
+ * var slug = jQuery([name=slug]).slug();
+ *
+ * slug.on('slugify', function (event, current, previous) {
+ * console.log("value was: %s, and is now %s", current, previous);
+ * });
+ *
+ * Returns the jQuery collection.
+ */
+(function ($) {
+
+ /* Handles the on change event that "slugifies" the entire string. This
+ * catches text pasted into the input.
+ *
+ * event - the DOM event object.
+ *
+ * Returns nothing.
+ */
+ function onChange(event) {
+ var value = this.value;
+ var updated = $.url.slugify(value, true);
+
+ if (value !== updated) {
+ this.value = updated;
+ $(this).trigger('slugify', [this.value, value]);
+ }
+ }
+
+ /* Handles the keypress event that will convert each character as the user
+ * inputs new text. This will not catch text pasted into the input.
+ *
+ * event - the DOM event object.
+ *
+ * Returns nothing.
+ */
+ function onKeypress(event) {
+ if (!event.charCode) {
+ return;
+ }
+
+ event.preventDefault();
+
+ var value = this.value;
+ var start = this.selectionStart;
+ var end = this.selectionEnd;
+ var char = String.fromCharCode(event.charCode);
+ var updated;
+ var range;
+
+ if (this.setSelectionRange) {
+ updated = value.substring(0, start) + char + value.substring(end, value.length);
+
+ this.value = $.url.slugify(updated, false);
+ this.setSelectionRange(start + 1, start + 1);
+ } else if (document.selection && document.selection.createRange) {
+ range = document.selection.createRange();
+ range.text = char + range.text;
+ }
+
+ $(this).trigger('slugify', [this.value, value]);
+ }
+
+ /* The jQuery plugin for converting an input.
+ */
+ $.fn.slug = function () {
+ return this.each(function () {
+ $(this).on({
+ 'blur.slug': onChange,
+ 'change.slug': onChange,
+ 'keypress.slug': onKeypress
+ });
+ });
+ };
+
+ // Export the methods onto the plugin for testability.
+ $.extend($.fn.slug, {onChange: onChange, onKeypress: onKeypress});
+})(this.jQuery);
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug.min.js
new file mode 100644
index 00000000..67b767d0
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.slug.min.js
@@ -0,0 +1,5 @@
+(function($){function onChange(event){var value=this.value;var updated=$.url.slugify(value,true);if(value!==updated){this.value=updated;$(this).trigger('slugify',[this.value,value]);}}
+function onKeypress(event){if(!event.charCode){return;}
+event.preventDefault();var value=this.value;var start=this.selectionStart;var end=this.selectionEnd;var char=String.fromCharCode(event.charCode);var updated;var range;if(this.setSelectionRange){updated=value.substring(0,start)+char+value.substring(end,value.length);this.value=$.url.slugify(updated,false);this.setSelectionRange(start+1,start+1);}else if(document.selection&&document.selection.createRange){range=document.selection.createRange();range.text=char+range.text;}
+$(this).trigger('slugify',[this.value,value]);}
+$.fn.slug=function(){return this.each(function(){$(this).on({'blur.slug':onChange,'change.slug':onChange,'keypress.slug':onKeypress});});};$.extend($.fn.slug,{onChange:onChange,onKeypress:onKeypress});})(this.jQuery);
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.truncator.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.truncator.js
new file mode 100644
index 00000000..983f45b2
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.truncator.js
@@ -0,0 +1,145 @@
+// HTML Truncator for jQuery
+// by Henrik Nyh
2008-02-28.
+// Free to modify and redistribute with credit.
+
+// EDIT: This plug-in has been modified from the original source to enable
+// some additional functionality.
+//
+// a) We now return the newly created "truncated" elements as a jQuery
+// collection. This can be restored as usual using .end().
+// b) We trigger the "expand.truncate" and "collapse.truncate" events when
+// the occur. The event object has an additional .relatedTarget property
+// which is the original expanded element.
+// c) We add an "ellipses" option that places the ellipses outside of the
+// expand/collapse links.
+//
+// We do this because this is the best plug-in I've found that handles
+// truncation of elements containing HTML components. Even better would be
+// to find one that can also be provided with a number of lines.
+//
+// Requirements are:
+//
+// 1. Must truncate the contents of an element keeping elements intact.
+// 2. Must be extensible trigger events when expand/collapse occurs.
+// 3. Truncate to a set number of lines rather than just characters.
+//
+(function($) {
+
+ var trailing_whitespace = true;
+
+ $.fn.truncate = function(options) {
+
+ var opts = $.extend({}, $.fn.truncate.defaults, options);
+
+ var collected = this.map(function() {
+
+ var content_length = $.trim(squeeze($(this).text())).length;
+ if (content_length <= opts.max_length)
+ return; // bail early if not overlong
+
+ // include more text, link prefix, and link suffix in max length
+ var actual_max_length = opts.max_length - opts.more.length - opts.link_prefix.length - opts.link_suffix.length;
+
+ var truncated_node = recursivelyTruncate(this, actual_max_length);
+ var full_node = $(this).hide();
+
+ truncated_node.insertAfter(full_node);
+
+ findNodeForMore(truncated_node).append(opts.ellipses + opts.link_prefix+''+opts.more+' '+opts.link_suffix);
+ findNodeForLess(full_node).append(opts.link_prefix+''+opts.less+' '+opts.link_suffix);
+
+ truncated_node.find('a:last').click(function(event) {
+ event.preventDefault();
+ truncated_node.hide(); full_node.show();
+
+ // Trigger an event for extensibility.
+ truncated_node.trigger({
+ type: 'expand.truncate',
+ relatedTarget: full_node[0]
+ });
+ });
+ full_node.find('a:last').click(function(event) {
+ event.preventDefault();
+ truncated_node.show(); full_node.hide();
+
+ // Trigger an event for extensibility.
+ truncated_node.trigger({
+ type: 'collapse.truncate',
+ relatedTarget: full_node[0]
+ });
+ });
+
+ // Return our new truncated node.
+ return truncated_node[0];
+ });
+
+ // Return the newly created elements.
+ return this.pushStack(collected);
+ }
+
+ // Note that the " (…more)" bit counts towards the max length – so a max
+ // length of 10 would truncate "1234567890" to "12 (…more)".
+ $.fn.truncate.defaults = {
+ max_length: 100,
+ more: 'more',
+ less: 'less',
+ ellipses: '…',
+ css_more_class: 'truncator-link truncator-more',
+ css_less_class: 'truncator-link truncator-less',
+ link_prefix: ' (',
+ link_suffix: ')'
+ };
+
+ function recursivelyTruncate(node, max_length) {
+ return (node.nodeType == 3) ? truncateText(node, max_length) : truncateNode(node, max_length);
+ }
+
+ function truncateNode(node, max_length) {
+ var node = $(node);
+ var new_node = node.clone().empty();
+ var truncatedChild;
+ node.contents().each(function() {
+ var remaining_length = max_length - new_node.text().length;
+ if (remaining_length == 0) return; // breaks the loop
+ truncatedChild = recursivelyTruncate(this, remaining_length);
+ if (truncatedChild) new_node.append(truncatedChild);
+ });
+ return new_node;
+ }
+
+ function truncateText(node, max_length) {
+ var text = squeeze(node.data);
+ if (trailing_whitespace) // remove initial whitespace if last text
+ text = text.replace(/^ /, ''); // node had trailing whitespace.
+ trailing_whitespace = !!text.match(/ $/);
+ var text = text.slice(0, max_length);
+ // Ensure HTML entities are encoded
+ // http://debuggable.com/posts/encode-html-entities-with-jquery:480f4dd6-13cc-4ce9-8071-4710cbdd56cb
+ text = $('
').text(text).html();
+ return text;
+ }
+
+ // Collapses a sequence of whitespace into a single space.
+ function squeeze(string) {
+ return string.replace(/\s+/g, ' ');
+ }
+
+ // Finds the last, innermost block-level element
+ function findNodeForMore(node) {
+ var $node = $(node);
+ var last_child = $node.children(":last");
+ if (!last_child) return node;
+ var display = last_child.css('display');
+ if (!display || display=='inline') return $node;
+ return findNodeForMore(last_child);
+ };
+
+ // Finds the last child if it's a p; otherwise the parent
+ function findNodeForLess(node) {
+ var $node = $(node);
+ var last_child = $node.children(":last");
+ if (last_child && last_child.is('p')) return last_child;
+ return node;
+ };
+
+})(jQuery);
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.truncator.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.truncator.min.js
new file mode 100644
index 00000000..be47cdf7
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.truncator.min.js
@@ -0,0 +1,8 @@
+(function($){var trailing_whitespace=true;$.fn.truncate=function(options){var opts=$.extend({},$.fn.truncate.defaults,options);var collected=this.map(function(){var content_length=$.trim(squeeze($(this).text())).length;if(content_length<=opts.max_length)
+return;var actual_max_length=opts.max_length-opts.more.length-opts.link_prefix.length-opts.link_suffix.length;var truncated_node=recursivelyTruncate(this,actual_max_length);var full_node=$(this).hide();truncated_node.insertAfter(full_node);findNodeForMore(truncated_node).append(opts.ellipses+opts.link_prefix+''+opts.more+' '+opts.link_suffix);findNodeForLess(full_node).append(opts.link_prefix+''+opts.less+' '+opts.link_suffix);truncated_node.find('a:last').click(function(event){event.preventDefault();truncated_node.hide();full_node.show();truncated_node.trigger({type:'expand.truncate',relatedTarget:full_node[0]});});full_node.find('a:last').click(function(event){event.preventDefault();truncated_node.show();full_node.hide();truncated_node.trigger({type:'collapse.truncate',relatedTarget:full_node[0]});});return truncated_node[0];});return this.pushStack(collected);}
+$.fn.truncate.defaults={max_length:100,more:'more',less:'less',ellipses:'…',css_more_class:'truncator-link truncator-more',css_less_class:'truncator-link truncator-less',link_prefix:' (',link_suffix:')'};function recursivelyTruncate(node,max_length){return(node.nodeType==3)?truncateText(node,max_length):truncateNode(node,max_length);}
+function truncateNode(node,max_length){var node=$(node);var new_node=node.clone().empty();var truncatedChild;node.contents().each(function(){var remaining_length=max_length-new_node.text().length;if(remaining_length==0)return;truncatedChild=recursivelyTruncate(this,remaining_length);if(truncatedChild)new_node.append(truncatedChild);});return new_node;}
+function truncateText(node,max_length){var text=squeeze(node.data);if(trailing_whitespace)
+text=text.replace(/^ /,'');trailing_whitespace=!!text.match(/ $/);var text=text.slice(0,max_length);text=$('
').text(text).html();return text;}
+function squeeze(string){return string.replace(/\s+/g,' ');}
+function findNodeForMore(node){var $node=$(node);var last_child=$node.children(":last");if(!last_child)return node;var display=last_child.css('display');if(!display||display=='inline')return $node;return findNodeForMore(last_child);};function findNodeForLess(node){var $node=$(node);var last_child=$node.children(":last");if(last_child&&last_child.is('p'))return last_child;return node;};})(jQuery);
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.url-helpers.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.url-helpers.js
new file mode 100644
index 00000000..f92d6292
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.url-helpers.js
@@ -0,0 +1,167 @@
+/* .slugify() based on jQuery Slugify a string! by Pablo Bandin
+ *
+ * See: http://tracehello.wordpress.com/2011/06/15/jquery-real-slugify-plugin/
+ *
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ */
+(function ($, window) {
+ $.url = {
+ /* Escapes a string for use in a url component. All special characters
+ * are url encoded and spaces are replaced by a plus rather than a %20.
+ *
+ * string - A string to convert.
+ *
+ * Examples
+ *
+ * jQuery.url.escape('apples & pears'); //=> "apples+%26+pears"
+ *
+ * Returns the escaped string.
+ */
+ escape: function (string) {
+ return window.encodeURIComponent(string || '').replace(/%20/g, '+');
+ },
+
+ /* Converts a string into a url compatible slug. Characters that cannot
+ * be converted will be replaced by hyphens.
+ *
+ * string - The string to convert.
+ * trim - Remove starting, trailing and duplicate hyphens (default: true)
+ *
+ * Examples
+ *
+ * jQuery.url.slugify('apples & pears'); //=> 'apples-pears'
+ *
+ * Returns the new slug.
+ */
+ slugify: function (string, trim) {
+ var str = '';
+ var index = 0;
+ var length = string.length;
+ var map = this.map;
+
+ for (;index < length; index += 1) {
+ str += map[string.charCodeAt(index).toString(16)] || '-';
+ }
+
+ str = str.toLowerCase();
+
+ return trim === false ? str : str.replace(/\-+/g, '-').replace(/^-|-$/g, '');
+ }
+ };
+
+ // The following takes two sets of characters, the first a set of hexadecimal
+ // Unicode character points, the second their visually similar counterparts.
+ // I'm not 100% sure this is the best way to handle such characters but
+ // it seems to be a common practice.
+ var unicode = ('20 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 ' +
+ '47 48 49 50 51 52 53 54 55 56 57 58 59 61 62 63 64 65 66 67 68 69 70 ' +
+ '71 72 73 74 75 76 77 78 79 100 101 102 103 104 105 106 107 108 109 ' +
+ '110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 ' +
+ '126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 ' +
+ '142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 ' +
+ '158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 ' +
+ '174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 ' +
+ '190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 ' +
+ '206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 ' +
+ '222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 ' +
+ '238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 ' +
+ '254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 ' +
+ '270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 ' +
+ '286 287 288 289 290 291 292 293 294 295 296 297 298 299 363 364 ' +
+ '365 366 367 368 369 386 388 389 390 391 392 393 394 395 396 397 ' +
+ '398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 ' +
+ '414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 ' +
+ '430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 ' +
+ '446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 ' +
+ '462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 ' +
+ '478 479 480 481 490 491 492 493 494 495 496 497 498 499 500 501 ' +
+ '502 503 504 505 506 507 508 509 510 511 512 513 514 515 531 532 ' +
+ '533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 ' +
+ '549 550 551 552 553 554 555 556 561 562 563 564 565 566 567 568 ' +
+ '569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 ' +
+ '585 586 587 4a 4b 4c 4d 4e 4f 5a 6a 6b 6c 6d 6e 6f 7a a2 a3 a5 a7 ' +
+ 'a9 aa ae b2 b3 b5 b6 b9 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ' +
+ 'ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 ' +
+ 'e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f8 f9 fa ' +
+ 'fb fc fd ff 10a 10b 10c 10d 10e 10f 11a 11b 11c 11d 11e 11f 12a ' +
+ '12b 12c 12d 12e 12f 13a 13b 13c 13d 13e 13f 14a 14b 14c 14d 14e ' +
+ '14f 15a 15b 15c 15d 15e 15f 16a 16b 16c 16d 16e 16f 17a 17b 17c ' +
+ '17d 17e 17f 18a 18b 18c 18d 18e 18f 19a 19b 19c 19d 19e 19f 1a0 ' +
+ '1a1 1a2 1a3 1a4 1a5 1a6 1a7 1a8 1a9 1aa 1ab 1ac 1ad 1ae 1af 1b0 ' +
+ '1b1 1b2 1b3 1b4 1b5 1b6 1b7 1b8 1b9 1ba 1bb 1bc 1bd 1be 1bf 1c4 ' +
+ '1c5 1c6 1c7 1c8 1c9 1ca 1cb 1cc 1cd 1ce 1cf 1d0 1d1 1d2 1d3 1d4 ' +
+ '1d5 1d6 1d7 1d8 1d9 1da 1db 1dc 1dd 1de 1df 1e0 1e1 1e2 1e3 1e4 ' +
+ '1e5 1e6 1e7 1e8 1e9 1ea 1eb 1ec 1ed 1ee 1ef 1f0 1f1 1f2 1f3 1f4 ' +
+ '1f5 1f6 1f7 1f8 1f9 1fa 1fb 1fc 1fd 1fe 1ff 20a 20b 20c 20d 20e ' +
+ '20f 21a 21b 21c 21d 21e 21f 22a 22b 22c 22d 22e 22f 23a 23b 23c ' +
+ '23d 23e 23f 24a 24b 24c 24d 24e 24f 25a 25b 25c 25d 25e 25f 26a ' +
+ '26b 26c 26d 26e 26f 27a 27b 27c 27d 27e 27f 28a 28b 28c 28d 28e ' +
+ '28f 29a 29b 29c 29d 29e 29f 2a0 2a1 2a2 2a3 2a4 2a5 2a6 2a7 2a8 ' +
+ '2a9 2aa 2ab 2ac 2ae 2af 2b0 2b1 2b2 2b3 2b4 2b5 2b6 2b7 2b8 2df ' +
+ '2e0 2e1 2e2 2e3 2e4 36a 36b 36c 36d 36e 36f 37b 37c 37d 38a 38c ' +
+ '38e 38f 39a 39b 39c 39d 39e 39f 3a0 3a1 3a3 3a4 3a5 3a6 3a7 3a8 ' +
+ '3a9 3aa 3ab 3ac 3ad 3ae 3af 3b0 3b1 3b2 3b3 3b4 3b5 3b6 3b7 3b8 ' +
+ '3b9 3ba 3bb 3bc 3bd 3be 3bf 3c0 3c1 3c2 3c3 3c4 3c5 3c6 3c7 3c8 ' +
+ '3c9 3ca 3cb 3cc 3cd 3ce 3d0 3d1 3d2 3d3 3d4 3d5 3d6 3d7 3d8 3d9 ' +
+ '3da 3db 3dc 3dd 3de 3df 3e2 3e3 3e4 3e5 3e6 3e7 3e8 3e9 3ea 3eb ' +
+ '3ec 3ed 3ee 3ef 3f0 3f1 3f2 3f3 3f4 3f5 3f6 3f7 3f8 3f9 3fa 3fb ' +
+ '3fc 3fd 3fe 3ff 40a 40b 40c 40d 40e 40f 41a 41b 41c 41d 41e 41f ' +
+ '42a 42b 42c 42d 42e 42f 43a 43b 43c 43d 43e 43f 44a 44b 44c 44d ' +
+ '44e 44f 45a 45b 45c 45d 45e 45f 46a 46b 46c 46d 46e 46f 47a 47b ' +
+ '47c 47d 47e 47f 48a 48b 48c 48d 48e 48f 49a 49b 49c 49d 49e 49f ' +
+ '4a0 4a1 4a2 4a3 4a4 4a5 4a6 4a7 4a8 4a9 4aa 4ab 4ac 4ad 4ae 4af ' +
+ '4b0 4b1 4b2 4b3 4b4 4b5 4b6 4b7 4b8 4b9 4ba 4bb 4bc 4bd 4be 4bf ' +
+ '4c0 4c1 4c2 4c3 4c4 4c5 4c6 4c7 4c8 4c9 4ca 4cb 4cc 4cd 4ce 4cf ' +
+ '4d0 4d1 4d2 4d3 4d4 4d5 4d6 4d7 4d8 4d9 4da 4db 4dc 4dd 4de 4df ' +
+ '4e0 4e1 4e2 4e3 4e4 4e5 4e6 4e7 4e8 4e9 4ea 4eb 4ec 4ed 4ee 4ef ' +
+ '4f0 4f1 4f2 4f3 4f4 4f5 4f6 4f7 4f8 4f9 4fa 4fb 4fc 4fd 4fe 4ff ' +
+ '50a 50b 50c 50d 50e 50f 51a 51b 51c 51d 53a 53b 53c 53d 53e 53f ' +
+ '54a 54b 54c 54d 54e 54f 56a 56b 56c 56d 56e 56f 57a 57b 57c 57d ' +
+ '57e 57f 5f').split(' ');
+
+ var replacement = ('- 0 1 2 3 4 5 6 7 8 9 A B C D E F G H I P Q R S T ' +
+ 'U V W X Y a b c d e f g h i p q r s t u v w x y A a A a A a C c C c ' +
+ 'D d E e E e E e E e G g G g H h H h I i I i IJ ij J j K k k L l L l ' +
+ 'N n N n N n n O o OE oe R r R r R r S s T t T t T t U u U u U u W w ' +
+ 'Y y Y Z b B b b b b C C c D E F f G Y h i I K k A a A a E e E e I i ' +
+ 'R r R r U u U u S s n d 8 8 Z z A a E e O o Y y l n t j db qp < ? ? ' +
+ 'B U A E e J j a a a b c e d d e e g g g Y x u h h i i w m n n N o oe ' +
+ 'm o r R R S f f f f t t u Z Z 3 3 ? ? 5 C O B a e i o u c d A ' +
+ 'E H i A B r A E Z H O I E E T r E S I I J jb A B B r D E X 3 N N P ' +
+ 'C T y O X U h W W a 6 B r d e x 3 N N P C T Y qp x U h W W e e h r ' +
+ 'e s i i j jb W w Tb tb IC ic A a IA ia Y y O o V v V v Oy oy C c R ' +
+ 'r F f H h X x 3 3 d d d d R R R R JT JT E e JT jt JX JX U D Q N T ' +
+ '2 F r p z 2 n x U B j t n C R 8 R O P O S w f q n t q t n p h a n ' +
+ 'a u j u 2 n 2 n g l uh p o S u J K L M N O Z j k l m n o z c f Y s ' +
+ 'c a r 2 3 u p 1 A A A A A A AE C E E E E I I I I D N O O O O O X O ' +
+ 'U U U U Y p b a a a a a a ae c e e e e i i i i o n o o o o o o u u ' +
+ 'u u y y C c C c D d E e G g G g I i I i I i l L l L l L n n O o O ' +
+ 'o S s S s S s U u U u U u z Z z Z z f D d d q E e l h w N n O O o ' +
+ 'P P P p R S s E l t T t T U u U U Y y Z z 3 3 3 3 2 5 5 5 p DZ Dz ' +
+ 'dz Lj Lj lj NJ Nj nj A a I i O o U u U u U u U u U u e A a A a AE ' +
+ 'ae G g G g K k Q q Q q 3 3 J dz dZ DZ g G h p N n A a AE ae O o I ' +
+ 'i O o O o T t 3 3 H h O o O o O o A C c L T s Q q R r Y y e 3 3 3 ' +
+ '3 j i I I I h w R r R R r r u v A M Y Y B G H j K L q ? c dz d3 dz ' +
+ 'ts tf tc fn ls lz ww u u h h j r r r R W Y x Y 1 s x c h m r t v x ' +
+ 'c c c I O Y O K A M N E O TT P E T Y O X Y O I Y a e n i v a b y d ' +
+ 'e c n 0 1 k j u v c o tt p s o t u q X Y w i u o u w b e Y Y Y O w ' +
+ 'x Q q C c F f N N W w q q h e S s X x 6 6 t t x e c j O E E p p C ' +
+ 'M M p C C C Hb Th K N Y U K jI M H O TT b bI b E IO R K JI M H O N ' +
+ 'b bI b e io r Hb h k n y u mY my Im Im 3 3 O o W w W W H H B b P p ' +
+ 'K k K k K k K k H h H h Ih ih O o C c T t Y y Y y X x TI ti H h H ' +
+ 'h H h E e E e I X x K k jt jt H h H h H h M m l A a A a AE ae E e ' +
+ 'e e E e X X 3 3 3 3 N n N n O o O o O o E e Y y Y y Y y H h R r bI ' +
+ 'bi F f X x X x H h G g T t Q q W w d r L Iu O y m o N U Y S d h l ' +
+ 'lu d y w 2 n u y un _').split(' ');
+
+ // Map the Unicode characters to their counterparts in an object.
+ var map = {};
+ for (var index = 0, length = unicode.length; index < length; index += 1) {
+ map[unicode[index]] = replacement[index];
+ }
+
+ $.url.map = map;
+
+})(this.jQuery, this);
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.url-helpers.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.url-helpers.min.js
new file mode 100644
index 00000000..904a62a4
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/javascript/plugins/jquery.url-helpers.min.js
@@ -0,0 +1,3 @@
+(function($,window){$.url={escape:function(string){return window.encodeURIComponent(string||'').replace(/%20/g,'+');},slugify:function(string,trim){var str='';var index=0;var length=string.length;var map=this.map;for(;indextermites');
+ });
+
+ it('should return the string with each instance of the term wrapped in bold tags', function () {
+ var target = this.module.formatResult({id: 'we have a termite terminology', text: 'we have a termite terminology'});
+ assert.equal(target, 'we have a term ite term inology');
+ });
+
+ it('should return the term if there is no last term saved', function () {
+ delete this.module._lastTerm;
+ var target = this.module.formatResult({id: 'we have a termite terminology', text: 'we have a termite terminology'});
+ assert.equal(target, 'we have a termite terminology');
+ });
+ });
+
+ describe('.formatNoMatches(term)', function () {
+ it('should return the no matches string if there is a term', function () {
+ var target = this.module.formatNoMatches('term');
+ assert.equal(target, 'No matches found');
+ });
+
+ it('should return the empty string if there is no term', function () {
+ var target = this.module.formatNoMatches('');
+ assert.equal(target, 'Start typing…');
+ });
+ });
+
+ describe('.formatInputTooShort(term, min)', function () {
+ it('should return the plural input too short string', function () {
+ var target = this.module.formatInputTooShort('term', 2);
+ assert.equal(target, 'Input is too short, must be at least 2 characters');
+ });
+
+ it('should return the singular input too short string', function () {
+ var target = this.module.formatInputTooShort('term', 1);
+ assert.equal(target, 'Input is too short, must be at least one character');
+ });
+ });
+
+ describe('.formatTerm()', function () {
+ it('should return an item object with id and text properties', function () {
+ assert.deepEqual(this.module.formatTerm('test'), {id: 'test', text: 'test'});
+ });
+
+ it('should trim whitespace from the value', function () {
+ assert.deepEqual(this.module.formatTerm(' test '), {id: 'test', text: 'test'});
+ });
+
+ it('should convert commas in ids into unicode characters', function () {
+ assert.deepEqual(this.module.formatTerm('test, test'), {id: 'test\u002C test', text: 'test, test'});
+ });
+ });
+
+ describe('.formatInitialValue(element, callback)', function () {
+ beforeEach(function () {
+ this.callback = sinon.spy();
+ });
+
+ it('should pass an item object with id and text properties into the callback', function () {
+ var target = jQuery(' ');
+
+ this.module.formatInitialValue(target, this.callback);
+ assert.calledWith(this.callback, {id: 'test', text: 'test'});
+ });
+
+ it('should pass an array of properties into the callback if options.tags is true', function () {
+ this.module.options.tags = true;
+ var target = jQuery(' ', {value: "test, test"});
+
+ this.module.formatInitialValue(target, this.callback);
+ assert.calledWith(this.callback, [{id: 'test', text: 'test'}, {id: 'test', text: 'test'}]);
+ });
+
+ it('should return the value if no callback is provided (to support select2 v2.1)', function () {
+ var target = jQuery(' ');
+
+ assert.deepEqual(this.module.formatInitialValue(target), {id: 'test', text: 'test'});
+ });
+ });
+
+ describe('._onQuery(options)', function () {
+ it('should lookup the current term with the callback', function () {
+ var target = sinon.stub(this.module, 'lookup');
+
+ this.module._onQuery({term: 'term', callback: 'callback'});
+
+ assert.called(target);
+ assert.calledWith(target, 'term', 'callback');
+ });
+
+ it('should do nothing if there is no options object', function () {
+ var target = sinon.stub(this.module, 'lookup');
+ this.module._onQuery();
+ assert.notCalled(target);
+ });
+ });
+
+ describe('._onKeydown(event)', function () {
+ beforeEach(function () {
+ this.keyDownEvent = jQuery.Event("keydown", { key: ',', which: 188 });
+ this.fakeEvent = {};
+ this.clock = sinon.useFakeTimers();
+ this.jQuery = sinon.stub(jQuery.fn, 'init', jQuery.fn.init);
+ this.Event = sinon.stub(jQuery, 'Event').returns(this.fakeEvent);
+ this.trigger = sinon.stub(jQuery.fn, 'trigger');
+ });
+
+ afterEach(function () {
+ this.clock.restore();
+ this.jQuery.restore();
+ this.Event.restore();
+ this.trigger.restore();
+ });
+
+ it('should trigger fake "return" keypress if a comma is pressed', function () {
+ this.module._onKeydown(this.keyDownEvent);
+
+ this.clock.tick(100);
+
+ assert.called(this.jQuery);
+ assert.called(this.Event);
+ assert.called(this.trigger);
+ assert.calledWith(this.trigger, this.fakeEvent);
+ });
+
+ it('should do nothing if another key is pressed', function () {
+ this.keyDownEvent.key = '╚';
+ this.keyDownEvent.which = 200;
+
+ this.module._onKeydown(this.keyDownEvent);
+
+ this.clock.tick(100);
+
+ assert.notCalled(this.Event);
+ });
+
+ it('should do nothing if key is pressed which has the comma key-code but is not a comma', function () {
+ this.keyDownEvent.key = 'ת';
+ this.keyDownEvent.which = 188;
+
+ this.module._onKeydown(this.keyDownEvent);
+
+ this.clock.tick(100);
+
+ assert.notCalled(this.Event);
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/autocomplete.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/autocomplete.spec.min.js
new file mode 100644
index 00000000..8c989d5b
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/autocomplete.spec.min.js
@@ -0,0 +1,2 @@
+describe('ckan.modules.AutocompleteModule()',function(){var Autocomplete=ckan.module.registry['autocomplete'];beforeEach(function(){if(jQuery.fn.select2){this.select2=sinon.stub(jQuery.fn,'select2');}else{this.select2=jQuery.fn.select2=sinon.stub().returns({data:sinon.stub().returns({on:sinon.stub()})});}
+this.el=document.createElement('input');this.sandbox=ckan.sandbox();this.sandbox.body=this.fixture;this.module=new Autocomplete(this.el,{},this.sandbox);});afterEach(function(){this.module.teardown();if(this.select2.restore){this.select2.restore();}else{delete jQuery.fn.select2;}});describe('.initialize()',function(){it('should bind callback methods to the module',function(){var target=sinon.stub(jQuery,'proxyAll');this.module.initialize();assert.called(target);assert.calledWith(target,this.module,/_on/,/format/);target.restore();});it('should setup the autocomplete plugin',function(){var target=sinon.stub(this.module,'setupAutoComplete');this.module.initialize();assert.called(target);});});describe('.setupAutoComplete()',function(){it('should initialize the autocomplete plugin',function(){this.module.setupAutoComplete();assert.called(this.select2);assert.calledWith(this.select2,{width:'resolve',query:this.module._onQuery,dropdownCssClass:'',containerCssClass:'',formatResult:this.module.formatResult,formatNoMatches:this.module.formatNoMatches,formatInputTooShort:this.module.formatInputTooShort,createSearchChoice:this.module.formatTerm,initSelection:this.module.formatInitialValue});});it('should initialize the autocomplete plugin with a tags callback if options.tags is true',function(){this.module.options.tags=true;this.module.setupAutoComplete();assert.called(this.select2);assert.calledWith(this.select2,{width:'resolve',tags:this.module._onQuery,dropdownCssClass:'',containerCssClass:'',formatResult:this.module.formatResult,formatNoMatches:this.module.formatNoMatches,formatInputTooShort:this.module.formatInputTooShort,initSelection:this.module.formatInitialValue});it('should watch the keydown event on the select2 input');it('should allow a custom css class to be added to the dropdown',function(){this.module.options.dropdownClass='tags';this.module.setupAutoComplete();assert.called(this.select2);assert.calledWith(this.select2,{width:'resolve',tags:this.module._onQuery,dropdownCssClass:'tags',containerCssClass:'',formatResult:this.module.formatResult,formatNoMatches:this.module.formatNoMatches,formatInputTooShort:this.module.formatInputTooShort,initSelection:this.module.formatInitialValue});});it('should allow a custom css class to be added to the container',function(){this.module.options.containerClass='tags';this.module.setupAutoComplete();assert.called(this.select2);assert.calledWith(this.select2,{width:'resolve',tags:this.module._onQuery,dropdownCssClass:'',containerCssClass:'tags',formatResult:this.module.formatResult,formatNoMatches:this.module.formatNoMatches,formatInputTooShort:this.module.formatInputTooShort,initSelection:this.module.formatInitialValue});});});});describe('.getCompletions(term, fn)',function(){beforeEach(function(){this.term='term';this.module.options.source='http://example.com?term=?';this.target=sinon.stub(this.sandbox.client,'getCompletions');});it('should get the completions from the client',function(){this.module.getCompletions(this.term);assert.called(this.target);});it('should replace the last ? in the source url with the term',function(){this.module.getCompletions(this.term);assert.calledWith(this.target,'http://example.com?term=term');});it('should escape special characters in the term',function(){this.module.getCompletions('term with spaces');assert.calledWith(this.target,'http://example.com?term=term%20with%20spaces');});});describe('.lookup(term, fn)',function(){beforeEach(function(){sinon.stub(this.module,'getCompletions');this.target=sinon.spy();this.module.setupAutoComplete();});it('should set the _lastTerm property',function(){this.module.lookup('term',this.target);assert.equal(this.module._lastTerm,'term');});it('should call the fn immediately if there is no term',function(){this.module.lookup('',this.target);assert.called(this.target);assert.calledWith(this.target,{results:[]});});it('should debounce the request if there is a term');it('should cancel the last request');});describe('.formatResult(state)',function(){beforeEach(function(){this.module._lastTerm='term';});it('should return the string with the last term wrapped in bold tags',function(){var target=this.module.formatResult({id:'we have termites',text:'we have termites'});assert.equal(target,'we have term ites');});it('should return the string with each instance of the term wrapped in bold tags',function(){var target=this.module.formatResult({id:'we have a termite terminology',text:'we have a termite terminology'});assert.equal(target,'we have a term ite term inology');});it('should return the term if there is no last term saved',function(){delete this.module._lastTerm;var target=this.module.formatResult({id:'we have a termite terminology',text:'we have a termite terminology'});assert.equal(target,'we have a termite terminology');});});describe('.formatNoMatches(term)',function(){it('should return the no matches string if there is a term',function(){var target=this.module.formatNoMatches('term');assert.equal(target,'No matches found');});it('should return the empty string if there is no term',function(){var target=this.module.formatNoMatches('');assert.equal(target,'Start typing…');});});describe('.formatInputTooShort(term, min)',function(){it('should return the plural input too short string',function(){var target=this.module.formatInputTooShort('term',2);assert.equal(target,'Input is too short, must be at least 2 characters');});it('should return the singular input too short string',function(){var target=this.module.formatInputTooShort('term',1);assert.equal(target,'Input is too short, must be at least one character');});});describe('.formatTerm()',function(){it('should return an item object with id and text properties',function(){assert.deepEqual(this.module.formatTerm('test'),{id:'test',text:'test'});});it('should trim whitespace from the value',function(){assert.deepEqual(this.module.formatTerm(' test '),{id:'test',text:'test'});});it('should convert commas in ids into unicode characters',function(){assert.deepEqual(this.module.formatTerm('test, test'),{id:'test\u002C test',text:'test, test'});});});describe('.formatInitialValue(element, callback)',function(){beforeEach(function(){this.callback=sinon.spy();});it('should pass an item object with id and text properties into the callback',function(){var target=jQuery(' ');this.module.formatInitialValue(target,this.callback);assert.calledWith(this.callback,{id:'test',text:'test'});});it('should pass an array of properties into the callback if options.tags is true',function(){this.module.options.tags=true;var target=jQuery(' ',{value:"test, test"});this.module.formatInitialValue(target,this.callback);assert.calledWith(this.callback,[{id:'test',text:'test'},{id:'test',text:'test'}]);});it('should return the value if no callback is provided (to support select2 v2.1)',function(){var target=jQuery(' ');assert.deepEqual(this.module.formatInitialValue(target),{id:'test',text:'test'});});});describe('._onQuery(options)',function(){it('should lookup the current term with the callback',function(){var target=sinon.stub(this.module,'lookup');this.module._onQuery({term:'term',callback:'callback'});assert.called(target);assert.calledWith(target,'term','callback');});it('should do nothing if there is no options object',function(){var target=sinon.stub(this.module,'lookup');this.module._onQuery();assert.notCalled(target);});});describe('._onKeydown(event)',function(){beforeEach(function(){this.keyDownEvent=jQuery.Event("keydown",{key:',',which:188});this.fakeEvent={};this.clock=sinon.useFakeTimers();this.jQuery=sinon.stub(jQuery.fn,'init',jQuery.fn.init);this.Event=sinon.stub(jQuery,'Event').returns(this.fakeEvent);this.trigger=sinon.stub(jQuery.fn,'trigger');});afterEach(function(){this.clock.restore();this.jQuery.restore();this.Event.restore();this.trigger.restore();});it('should trigger fake "return" keypress if a comma is pressed',function(){this.module._onKeydown(this.keyDownEvent);this.clock.tick(100);assert.called(this.jQuery);assert.called(this.Event);assert.called(this.trigger);assert.calledWith(this.trigger,this.fakeEvent);});it('should do nothing if another key is pressed',function(){this.keyDownEvent.key='╚';this.keyDownEvent.which=200;this.module._onKeydown(this.keyDownEvent);this.clock.tick(100);assert.notCalled(this.Event);});it('should do nothing if key is pressed which has the comma key-code but is not a comma',function(){this.keyDownEvent.key='ת';this.keyDownEvent.which=188;this.module._onKeydown(this.keyDownEvent);this.clock.tick(100);assert.notCalled(this.Event);});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/basic-form.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/basic-form.spec.js
new file mode 100644
index 00000000..9d0628fc
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/basic-form.spec.js
@@ -0,0 +1,39 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('ckan.module.BasicFormModule()', function () {
+ var BasicFormModule = ckan.module.registry['basic-form'];
+
+ beforeEach(function () {
+ sinon.stub(jQuery.fn, 'incompleteFormWarning');
+
+ this.el = document.createElement('form');
+ this.el.innerHTML = 'Save '
+ this.sandbox = ckan.sandbox();
+ this.sandbox.body = this.fixture;
+ this.sandbox.body.append(this.el)
+ this.module = new BasicFormModule(this.el, {}, this.sandbox);
+ });
+
+ afterEach(function () {
+ this.module.teardown();
+ jQuery.fn.incompleteFormWarning.restore();
+ });
+
+ describe('.initialize()', function () {
+ it('should attach the jQuery.fn.incompleteFormWarning() to the form', function () {
+ this.module.initialize();
+ assert.called(jQuery.fn.incompleteFormWarning);
+ });
+
+ it('should disable the submit button on form submit', function(done) {
+ this.module.initialize();
+ this.module._onSubmit();
+
+ setTimeout(function() {
+ var buttonAttrDisabled = this.el.querySelector('button').getAttribute('disabled');
+
+ assert.ok(buttonAttrDisabled === 'disabled')
+ done();
+ }.bind(this), 0);
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/basic-form.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/basic-form.spec.min.js
new file mode 100644
index 00000000..b104d251
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/basic-form.spec.min.js
@@ -0,0 +1,4 @@
+describe('ckan.module.BasicFormModule()',function(){var BasicFormModule=ckan.module.registry['basic-form'];beforeEach(function(){sinon.stub(jQuery.fn,'incompleteFormWarning');this.el=document.createElement('form');this.el.innerHTML='Save '
+this.sandbox=ckan.sandbox();this.sandbox.body=this.fixture;this.sandbox.body.append(this.el)
+this.module=new BasicFormModule(this.el,{},this.sandbox);});afterEach(function(){this.module.teardown();jQuery.fn.incompleteFormWarning.restore();});describe('.initialize()',function(){it('should attach the jQuery.fn.incompleteFormWarning() to the form',function(){this.module.initialize();assert.called(jQuery.fn.incompleteFormWarning);});it('should disable the submit button on form submit',function(done){this.module.initialize();this.module._onSubmit();setTimeout(function(){var buttonAttrDisabled=this.el.querySelector('button').getAttribute('disabled');assert.ok(buttonAttrDisabled==='disabled')
+done();}.bind(this),0);});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/confirm-action.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/confirm-action.spec.js
new file mode 100644
index 00000000..d90b36d6
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/confirm-action.spec.js
@@ -0,0 +1,118 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('ckan.module.ConfirmActionModule()', function () {
+ var ConfirmActionModule = ckan.module.registry['confirm-action'];
+
+ beforeEach(function () {
+ jQuery.fn.modal = sinon.spy();
+
+ this.el = document.createElement('button');
+ this.sandbox = ckan.sandbox();
+ this.sandbox.body = this.fixture;
+ this.module = new ConfirmActionModule(this.el, {}, this.sandbox);
+ });
+
+ afterEach(function () {
+ this.module.teardown();
+ });
+
+ describe('.initialize()', function () {
+ it('should watch for clicks on the module element', function () {
+ var target = sinon.stub(this.module.el, 'on');
+ this.module.initialize();
+ assert.called(target);
+ assert.calledWith(target, 'click', this.module._onClick);
+ });
+ });
+
+ describe('.confirm()', function () {
+ it('should append the modal to the document body', function () {
+ this.module.confirm();
+ assert.equal(this.fixture.children().length, 1);
+ assert.equal(this.fixture.find('.modal').length, 1);
+ });
+
+ it('should show the modal dialog', function () {
+ this.module.confirm();
+ assert.called(jQuery.fn.modal);
+ assert.calledWith(jQuery.fn.modal, 'show');
+ });
+ });
+
+ describe('.performAction()', function () {
+ it('should submit the action');
+ });
+
+ describe('.createModal()', function () {
+ it('should create the modal element', function () {
+ var target = this.module.createModal();
+
+ assert.ok(target.hasClass('modal'));
+ });
+
+ it('should set the module.modal property', function () {
+ var target = this.module.createModal();
+
+ assert.ok(target === this.module.modal);
+ });
+
+ it('should bind the success/cancel listeners', function () {
+ var target = sinon.stub(jQuery.fn, 'on');
+
+ this.module.createModal();
+
+ // Not an ideal check as this implementation could be done in many ways.
+ assert.calledTwice(target);
+ assert.calledWith(target, 'click', '.btn-primary', this.module._onConfirmSuccess);
+ assert.calledWith(target, 'click', '.btn-cancel', this.module._onConfirmCancel);
+
+ target.restore();
+ });
+
+ it('should initialise the modal plugin', function () {
+ this.module.createModal();
+ assert.called(jQuery.fn.modal);
+ assert.calledWith(jQuery.fn.modal, {show: false});
+ });
+
+ it('should allow to customize the content', function () {
+ this.module.options.content = 'some custom content';
+ var target = this.module.createModal();
+
+ assert.equal(target.find('.modal-body').text(), 'some custom content');
+ });
+ });
+
+ describe('._onClick()', function () {
+ it('should prevent the default action', function () {
+ var target = {preventDefault: sinon.spy()};
+ this.module._onClick(target);
+
+ assert.called(target.preventDefault);
+ });
+
+ it('should display the confirmation dialog', function () {
+ var target = sinon.stub(this.module, 'confirm');
+ this.module._onClick({preventDefault: sinon.spy()});
+ assert.called(target);
+ });
+ });
+
+ describe('._onConfirmSuccess()', function () {
+ it('should perform the action', function () {
+ var target = sinon.stub(this.module, 'performAction');
+ this.module._onConfirmSuccess(jQuery.Event('click'));
+ assert.called(target);
+ });
+ });
+
+ describe('._onConfirmCancel()', function () {
+ it('should hide the modal', function () {
+ this.module.modal = jQuery('
');
+ this.module._onConfirmCancel(jQuery.Event('click'));
+
+ assert.called(jQuery.fn.modal);
+ assert.calledWith(jQuery.fn.modal, 'hide');
+ });
+ });
+
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/confirm-action.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/confirm-action.spec.min.js
new file mode 100644
index 00000000..ad5a210f
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/confirm-action.spec.min.js
@@ -0,0 +1 @@
+describe('ckan.module.ConfirmActionModule()',function(){var ConfirmActionModule=ckan.module.registry['confirm-action'];beforeEach(function(){jQuery.fn.modal=sinon.spy();this.el=document.createElement('button');this.sandbox=ckan.sandbox();this.sandbox.body=this.fixture;this.module=new ConfirmActionModule(this.el,{},this.sandbox);});afterEach(function(){this.module.teardown();});describe('.initialize()',function(){it('should watch for clicks on the module element',function(){var target=sinon.stub(this.module.el,'on');this.module.initialize();assert.called(target);assert.calledWith(target,'click',this.module._onClick);});});describe('.confirm()',function(){it('should append the modal to the document body',function(){this.module.confirm();assert.equal(this.fixture.children().length,1);assert.equal(this.fixture.find('.modal').length,1);});it('should show the modal dialog',function(){this.module.confirm();assert.called(jQuery.fn.modal);assert.calledWith(jQuery.fn.modal,'show');});});describe('.performAction()',function(){it('should submit the action');});describe('.createModal()',function(){it('should create the modal element',function(){var target=this.module.createModal();assert.ok(target.hasClass('modal'));});it('should set the module.modal property',function(){var target=this.module.createModal();assert.ok(target===this.module.modal);});it('should bind the success/cancel listeners',function(){var target=sinon.stub(jQuery.fn,'on');this.module.createModal();assert.calledTwice(target);assert.calledWith(target,'click','.btn-primary',this.module._onConfirmSuccess);assert.calledWith(target,'click','.btn-cancel',this.module._onConfirmCancel);target.restore();});it('should initialise the modal plugin',function(){this.module.createModal();assert.called(jQuery.fn.modal);assert.calledWith(jQuery.fn.modal,{show:false});});it('should allow to customize the content',function(){this.module.options.content='some custom content';var target=this.module.createModal();assert.equal(target.find('.modal-body').text(),'some custom content');});});describe('._onClick()',function(){it('should prevent the default action',function(){var target={preventDefault:sinon.spy()};this.module._onClick(target);assert.called(target.preventDefault);});it('should display the confirmation dialog',function(){var target=sinon.stub(this.module,'confirm');this.module._onClick({preventDefault:sinon.spy()});assert.called(target);});});describe('._onConfirmSuccess()',function(){it('should perform the action',function(){var target=sinon.stub(this.module,'performAction');this.module._onConfirmSuccess(jQuery.Event('click'));assert.called(target);});});describe('._onConfirmCancel()',function(){it('should hide the modal',function(){this.module.modal=jQuery('
');this.module._onConfirmCancel(jQuery.Event('click'));assert.called(jQuery.fn.modal);assert.calledWith(jQuery.fn.modal,'hide');});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/custom-fields.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/custom-fields.spec.js
new file mode 100644
index 00000000..93afad1a
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/custom-fields.spec.js
@@ -0,0 +1,168 @@
+/*globals describe before beforeEach afterEach it assert sinon ckan jQuery */
+describe('ckan.module.CustomFieldsModule()', function () {
+ var CustomFieldsModule = ckan.module.registry['custom-fields'];
+
+ before(function (done) {
+ this.loadFixture('custom_fields.html', function (template) {
+ this.template = template;
+ done();
+ });
+ });
+
+ beforeEach(function () {
+ this.fixture.html(this.template);
+ this.el = this.fixture.find('[data-module]');
+ this.sandbox = ckan.sandbox();
+ this.sandbox.body = this.fixture;
+ this.module = new CustomFieldsModule(this.el, {}, this.sandbox);
+ });
+
+ afterEach(function () {
+ this.module.teardown();
+ });
+
+ describe('.initialize()', function () {
+ it('should bind all functions beginning with _on to the module scope', function () {
+ var target = sinon.stub(jQuery, 'proxyAll');
+
+ this.module.initialize();
+
+ assert.called(target);
+ assert.calledWith(target, this.module, /_on/);
+
+ target.restore();
+ });
+
+ it('should listen for changes to the last "key" input', function () {
+ var target = sinon.stub(this.module, '_onChange');
+
+ this.module.initialize();
+ this.module.$('input[name*=key]').change();
+
+ assert.calledOnce(target);
+ });
+
+ it('should listen for changes to all checkboxes', function () {
+ var target = sinon.stub(this.module, '_onRemove');
+
+ this.module.initialize();
+ this.module.$(':checkbox').trigger('change');
+
+ assert.calledOnce(target);
+ });
+ });
+
+ describe('.newField(element)', function () {
+ it('should append a new field to the element', function () {
+ var element = document.createElement('div');
+ sinon.stub(this.module, 'cloneField').returns(element);
+
+ this.module.newField();
+
+ assert.ok(jQuery.contains(this.module.el[0], element));
+ });
+ });
+
+ describe('.cloneField(element)', function () {
+ it('should clone the provided field', function () {
+ var element = document.createElement('div');
+ var init = sinon.stub(jQuery.fn, 'init', jQuery.fn.init);
+ var clone = sinon.stub(jQuery.fn, 'clone', jQuery.fn.clone);
+
+ this.module.cloneField(element);
+
+ assert.called(init);
+ assert.calledWith(init, element);
+ assert.called(clone);
+
+ init.restore();
+ clone.restore();
+ });
+
+ it('should return the cloned element', function () {
+ var element = document.createElement('div');
+ var cloned = document.createElement('div');
+ var init = sinon.stub(jQuery.fn, 'init', jQuery.fn.init);
+ var clone = sinon.stub(jQuery.fn, 'clone').returns(jQuery(cloned));
+
+ assert.ok(this.module.cloneField(element)[0] === cloned);
+
+ init.restore();
+ clone.restore();
+ });
+ });
+
+ describe('.resetField(element)', function () {
+ beforeEach(function () {
+ this.field = jQuery('Field 1
');
+ });
+
+ it('should empty all input values', function () {
+ var target = this.module.resetField(this.field);
+ assert.equal(target.find(':input').val(), '');
+ });
+
+ it('should increment any integers in the input names by one', function () {
+ var target = this.module.resetField(this.field);
+ assert.equal(target.find(':input').attr('name'), 'field-2');
+ });
+
+ it('should increment any numbers in the label text by one', function () {
+ var target = this.module.resetField(this.field);
+ assert.equal(target.find('label').text(), 'Field 2');
+ });
+
+ it('should increment any numbers in the label for by one', function () {
+ var target = this.module.resetField(this.field);
+ assert.equal(target.find('label').attr('for'), 'field-2');
+ });
+ });
+
+ describe('.disableField(field, disable)', function () {
+ beforeEach(function () {
+ this.target = this.module.$('.control-custom:first');
+ });
+
+ it('should add a .disable class to the element', function () {
+ this.module.disableField(this.target);
+ assert.isTrue(this.target.hasClass('disabled'));
+ });
+
+ it('should remove a .disable class to the element if disable is false', function () {
+ this.target.addClass('disable');
+
+ this.module.disableField(this.target, false);
+ assert.isFalse(this.target.hasClass('disabled'));
+ });
+
+ });
+
+ describe('._onChange(event)', function () {
+ it('should call .newField() with the custom control', function () {
+ var target = sinon.stub(this.module, 'newField');
+ var field = this.module.$('[name*=key]:last').val('test');
+
+ this.module._onChange(jQuery.Event('change', {target: field[0]}));
+
+ assert.called(target);
+ });
+
+ it('should not call .newField() if the target field is empty', function () {
+ var target = sinon.stub(this.module, 'newField');
+ var field = this.module.$('[name*=key]:last').val('');
+
+ this.module._onChange(jQuery.Event('change', {target: field[0]}));
+
+ assert.notCalled(target);
+ });
+ });
+
+ describe('._onRemove(event)', function () {
+ it('should call .disableField() with the custom control', function () {
+ var target = sinon.stub(this.module, 'disableField');
+ this.module._onRemove(jQuery.Event('change', {target: this.module.$(':checkbox')[0]}));
+
+ assert.called(target);
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/custom-fields.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/custom-fields.spec.min.js
new file mode 100644
index 00000000..7d846d50
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/custom-fields.spec.min.js
@@ -0,0 +1 @@
+describe('ckan.module.CustomFieldsModule()',function(){var CustomFieldsModule=ckan.module.registry['custom-fields'];before(function(done){this.loadFixture('custom_fields.html',function(template){this.template=template;done();});});beforeEach(function(){this.fixture.html(this.template);this.el=this.fixture.find('[data-module]');this.sandbox=ckan.sandbox();this.sandbox.body=this.fixture;this.module=new CustomFieldsModule(this.el,{},this.sandbox);});afterEach(function(){this.module.teardown();});describe('.initialize()',function(){it('should bind all functions beginning with _on to the module scope',function(){var target=sinon.stub(jQuery,'proxyAll');this.module.initialize();assert.called(target);assert.calledWith(target,this.module,/_on/);target.restore();});it('should listen for changes to the last "key" input',function(){var target=sinon.stub(this.module,'_onChange');this.module.initialize();this.module.$('input[name*=key]').change();assert.calledOnce(target);});it('should listen for changes to all checkboxes',function(){var target=sinon.stub(this.module,'_onRemove');this.module.initialize();this.module.$(':checkbox').trigger('change');assert.calledOnce(target);});});describe('.newField(element)',function(){it('should append a new field to the element',function(){var element=document.createElement('div');sinon.stub(this.module,'cloneField').returns(element);this.module.newField();assert.ok(jQuery.contains(this.module.el[0],element));});});describe('.cloneField(element)',function(){it('should clone the provided field',function(){var element=document.createElement('div');var init=sinon.stub(jQuery.fn,'init',jQuery.fn.init);var clone=sinon.stub(jQuery.fn,'clone',jQuery.fn.clone);this.module.cloneField(element);assert.called(init);assert.calledWith(init,element);assert.called(clone);init.restore();clone.restore();});it('should return the cloned element',function(){var element=document.createElement('div');var cloned=document.createElement('div');var init=sinon.stub(jQuery.fn,'init',jQuery.fn.init);var clone=sinon.stub(jQuery.fn,'clone').returns(jQuery(cloned));assert.ok(this.module.cloneField(element)[0]===cloned);init.restore();clone.restore();});});describe('.resetField(element)',function(){beforeEach(function(){this.field=jQuery('Field 1
');});it('should empty all input values',function(){var target=this.module.resetField(this.field);assert.equal(target.find(':input').val(),'');});it('should increment any integers in the input names by one',function(){var target=this.module.resetField(this.field);assert.equal(target.find(':input').attr('name'),'field-2');});it('should increment any numbers in the label text by one',function(){var target=this.module.resetField(this.field);assert.equal(target.find('label').text(),'Field 2');});it('should increment any numbers in the label for by one',function(){var target=this.module.resetField(this.field);assert.equal(target.find('label').attr('for'),'field-2');});});describe('.disableField(field, disable)',function(){beforeEach(function(){this.target=this.module.$('.control-custom:first');});it('should add a .disable class to the element',function(){this.module.disableField(this.target);assert.isTrue(this.target.hasClass('disabled'));});it('should remove a .disable class to the element if disable is false',function(){this.target.addClass('disable');this.module.disableField(this.target,false);assert.isFalse(this.target.hasClass('disabled'));});});describe('._onChange(event)',function(){it('should call .newField() with the custom control',function(){var target=sinon.stub(this.module,'newField');var field=this.module.$('[name*=key]:last').val('test');this.module._onChange(jQuery.Event('change',{target:field[0]}));assert.called(target);});it('should not call .newField() if the target field is empty',function(){var target=sinon.stub(this.module,'newField');var field=this.module.$('[name*=key]:last').val('');this.module._onChange(jQuery.Event('change',{target:field[0]}));assert.notCalled(target);});});describe('._onRemove(event)',function(){it('should call .disableField() with the custom control',function(){var target=sinon.stub(this.module,'disableField');this.module._onRemove(jQuery.Event('change',{target:this.module.$(':checkbox')[0]}));assert.called(target);});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/followers-counter.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/followers-counter.spec.js
new file mode 100644
index 00000000..d52edf6b
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/followers-counter.spec.js
@@ -0,0 +1,186 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('ckan.module.FollowersCounterModule()', function() {
+ var FollowersCounterModule = ckan.module.registry['followers-counter'];
+
+ beforeEach(function() {
+ this.initialCounter = 10;
+ this.el = jQuery('' + this.initialCounter + ' ');
+ this.sandbox = ckan.sandbox();
+ this.module = new FollowersCounterModule(this.el, {}, this.sandbox);
+ this.module.options.num_followers = this.initialCounter;
+ });
+
+ afterEach(function() {
+ this.module.teardown();
+ });
+
+ describe('.initialize()', function() {
+ it('should bind callback methods to the module', function() {
+ var target = sinon.stub(jQuery, 'proxyAll');
+
+ this.module.initialize();
+
+ assert.called(target);
+ assert.calledWith(target, this.module, /_on/);
+
+ target.restore();
+ });
+
+ it('should subscribe to the "follow-follow-some-id" event', function() {
+ var target = sinon.stub(this.sandbox, 'subscribe');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+
+ assert.called(target);
+ assert.calledWith(target, 'follow-follow-some-id', this.module._onFollow);
+
+ target.restore();
+ });
+
+ it('should subscribe to the "follow-unfollow-some-id" event', function() {
+ var target = sinon.stub(this.sandbox, 'subscribe');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+
+ assert.called(target);
+ assert.calledWith(target, 'follow-unfollow-some-id', this.module._onUnfollow);
+
+ target.restore();
+ });
+ });
+
+ describe('.teardown()', function() {
+ it('should unsubscribe to the "follow-follow-some-id" event', function() {
+ var target = sinon.stub(this.sandbox, 'unsubscribe');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+ this.module.teardown();
+
+ assert.called(target);
+ assert.calledWith(target, 'follow-follow-some-id', this.module._onFollow);
+
+ target.restore();
+ });
+
+ it('should unsubscribe to the "follow-unfollow-some-id" event', function() {
+ var target = sinon.stub(this.sandbox, 'unsubscribe');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+ this.module.teardown();
+
+ assert.called(target);
+ assert.calledWith(target, 'follow-unfollow-some-id', this.module._onUnfollow);
+
+ target.restore();
+ });
+ });
+
+ describe('._onFollow', function() {
+ it('should call _onFollow on "follow-follow-some-id" event', function() {
+ var target = sinon.stub(this.module, '_onFollow');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+
+ this.sandbox.publish('follow-follow-some-id');
+
+ assert.called(target);
+ });
+
+ it('should call _updateCounter when ._onFollow is called', function() {
+ var target = sinon.stub(this.module, '_updateCounter');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+
+ this.module._onFollow();
+
+ assert.called(target);
+ assert.calledWith(target, {action: 'follow'});
+ });
+ });
+
+ describe('._onUnfollow', function() {
+ it('should call _onUnfollow on "follow-unfollow-some-id" event', function() {
+ var target = sinon.stub(this.module, '_onUnfollow');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+
+ this.sandbox.publish('follow-unfollow-some-id');
+
+ assert.called(target);
+ });
+
+ it('should call _updateCounter when ._onUnfollow is called', function() {
+ var target = sinon.stub(this.module, '_updateCounter');
+
+ this.module.options = {id: 'some-id'};
+ this.module.initialize();
+
+ this.module._onUnfollow();
+
+ assert.called(target);
+ assert.calledWith(target, {action: 'unfollow'});
+ });
+ });
+
+ describe('._updateCounter', function() {
+ it('should increment this.options.num_followers on calling _onFollow', function() {
+ this.module.initialize();
+ this.module._onFollow();
+
+ assert.equal(this.module.options.num_followers, ++this.initialCounter);
+ });
+
+ it('should increment the counter value in the DOM on calling _onFollow', function() {
+ var counterVal;
+
+ this.module.initialize();
+ this.module._onFollow();
+
+ counterVal = this.module.counterEl.text();
+ counterVal = parseInt(counterVal, 10);
+
+ assert.equal(counterVal, ++this.initialCounter);
+ });
+
+ it('should decrement this.options.num_followers on calling _onUnfollow', function() {
+ this.module.initialize();
+ this.module._onUnfollow();
+
+ assert.equal(this.module.options.num_followers, --this.initialCounter);
+ });
+
+ it('should decrement the counter value in the DOM on calling _onUnfollow', function() {
+ var counterVal;
+
+ this.module.initialize();
+ this.module._onUnfollow();
+
+ counterVal = this.module.counterEl.text();
+ counterVal = parseInt(counterVal, 10);
+
+ assert.equal(counterVal, --this.initialCounter);
+ });
+
+ it('should not change the counter value in the DOM when the value is greater than 1000', function() {
+ var beforeCounterVal = 1536;
+ var afterCounterVal;
+
+ this.module.options = {num_followers: beforeCounterVal};
+ this.module.initialize();
+ this.module.counterEl.text(this.module.options.num_followers);
+ this.module._onFollow();
+
+ afterCounterVal = this.module.counterEl.text();
+ afterCounterVal = parseInt(afterCounterVal, 10);
+
+ assert.equal(beforeCounterVal, afterCounterVal);
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/followers-counter.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/followers-counter.spec.min.js
new file mode 100644
index 00000000..6745689a
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/followers-counter.spec.min.js
@@ -0,0 +1 @@
+describe('ckan.module.FollowersCounterModule()',function(){var FollowersCounterModule=ckan.module.registry['followers-counter'];beforeEach(function(){this.initialCounter=10;this.el=jQuery(''+this.initialCounter+' ');this.sandbox=ckan.sandbox();this.module=new FollowersCounterModule(this.el,{},this.sandbox);this.module.options.num_followers=this.initialCounter;});afterEach(function(){this.module.teardown();});describe('.initialize()',function(){it('should bind callback methods to the module',function(){var target=sinon.stub(jQuery,'proxyAll');this.module.initialize();assert.called(target);assert.calledWith(target,this.module,/_on/);target.restore();});it('should subscribe to the "follow-follow-some-id" event',function(){var target=sinon.stub(this.sandbox,'subscribe');this.module.options={id:'some-id'};this.module.initialize();assert.called(target);assert.calledWith(target,'follow-follow-some-id',this.module._onFollow);target.restore();});it('should subscribe to the "follow-unfollow-some-id" event',function(){var target=sinon.stub(this.sandbox,'subscribe');this.module.options={id:'some-id'};this.module.initialize();assert.called(target);assert.calledWith(target,'follow-unfollow-some-id',this.module._onUnfollow);target.restore();});});describe('.teardown()',function(){it('should unsubscribe to the "follow-follow-some-id" event',function(){var target=sinon.stub(this.sandbox,'unsubscribe');this.module.options={id:'some-id'};this.module.initialize();this.module.teardown();assert.called(target);assert.calledWith(target,'follow-follow-some-id',this.module._onFollow);target.restore();});it('should unsubscribe to the "follow-unfollow-some-id" event',function(){var target=sinon.stub(this.sandbox,'unsubscribe');this.module.options={id:'some-id'};this.module.initialize();this.module.teardown();assert.called(target);assert.calledWith(target,'follow-unfollow-some-id',this.module._onUnfollow);target.restore();});});describe('._onFollow',function(){it('should call _onFollow on "follow-follow-some-id" event',function(){var target=sinon.stub(this.module,'_onFollow');this.module.options={id:'some-id'};this.module.initialize();this.sandbox.publish('follow-follow-some-id');assert.called(target);});it('should call _updateCounter when ._onFollow is called',function(){var target=sinon.stub(this.module,'_updateCounter');this.module.options={id:'some-id'};this.module.initialize();this.module._onFollow();assert.called(target);assert.calledWith(target,{action:'follow'});});});describe('._onUnfollow',function(){it('should call _onUnfollow on "follow-unfollow-some-id" event',function(){var target=sinon.stub(this.module,'_onUnfollow');this.module.options={id:'some-id'};this.module.initialize();this.sandbox.publish('follow-unfollow-some-id');assert.called(target);});it('should call _updateCounter when ._onUnfollow is called',function(){var target=sinon.stub(this.module,'_updateCounter');this.module.options={id:'some-id'};this.module.initialize();this.module._onUnfollow();assert.called(target);assert.calledWith(target,{action:'unfollow'});});});describe('._updateCounter',function(){it('should increment this.options.num_followers on calling _onFollow',function(){this.module.initialize();this.module._onFollow();assert.equal(this.module.options.num_followers,++this.initialCounter);});it('should increment the counter value in the DOM on calling _onFollow',function(){var counterVal;this.module.initialize();this.module._onFollow();counterVal=this.module.counterEl.text();counterVal=parseInt(counterVal,10);assert.equal(counterVal,++this.initialCounter);});it('should decrement this.options.num_followers on calling _onUnfollow',function(){this.module.initialize();this.module._onUnfollow();assert.equal(this.module.options.num_followers,--this.initialCounter);});it('should decrement the counter value in the DOM on calling _onUnfollow',function(){var counterVal;this.module.initialize();this.module._onUnfollow();counterVal=this.module.counterEl.text();counterVal=parseInt(counterVal,10);assert.equal(counterVal,--this.initialCounter);});it('should not change the counter value in the DOM when the value is greater than 1000',function(){var beforeCounterVal=1536;var afterCounterVal;this.module.options={num_followers:beforeCounterVal};this.module.initialize();this.module.counterEl.text(this.module.options.num_followers);this.module._onFollow();afterCounterVal=this.module.counterEl.text();afterCounterVal=parseInt(afterCounterVal,10);assert.equal(beforeCounterVal,afterCounterVal);});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/image-upload.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/image-upload.spec.js
new file mode 100644
index 00000000..909550ff
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/image-upload.spec.js
@@ -0,0 +1,65 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('ckan.modules.ImageUploadModule()', function () {
+ var ImageUploadModule = ckan.module.registry['image-upload'];
+
+ beforeEach(function () {
+ this.el = document.createElement('div');
+ this.sandbox = ckan.sandbox();
+ this.module = new ImageUploadModule(this.el, {}, this.sandbox);
+ this.module.el.html([
+ '
',
+ ' ',
+ ]);
+ this.module.initialize();
+ this.module.field_name = jQuery(' ', {type: 'text'})
+ });
+
+ afterEach(function () {
+ this.module.teardown();
+ });
+
+ describe('._onFromWeb()', function () {
+
+ it('should change name when url changed', function () {
+ this.module.field_url_input.val('http://example.com/some_image.png');
+ this.module._onFromWebBlur();
+ assert.equal(this.module.field_name.val(), 'some_image.png');
+
+ this.module.field_url_input.val('http://example.com/undefined_file');
+ this.module._onFromWebBlur();
+ assert.equal(this.module.field_name.val(), 'undefined_file');
+ });
+
+ it('should ignore url changes if name was manualy changed', function () {
+ this.module.field_url_input.val('http://example.com/some_image.png');
+ this.module._onFromWebBlur();
+ assert.equal(this.module.field_name.val(), 'some_image.png');
+
+ this.module._onModifyName();
+
+ this.module.field_url_input.val('http://example.com/undefined_file');
+ this.module._onFromWebBlur();
+ assert.equal(this.module.field_name.val(), 'some_image.png');
+ });
+
+ it('should ignore url changes if name was filled before', function () {
+ this.module._nameIsDirty = true;
+ this.module.field_name.val('prefilled');
+
+ this.module.field_url_input.val('http://example.com/some_image.png');
+ this.module._onFromWebBlur();
+ assert.equal(this.module.field_name.val(), 'prefilled');
+
+ this.module.field_url_input.val('http://example.com/second_some_image.png');
+ this.module._onFromWebBlur();
+ assert.equal(this.module.field_name.val(), 'prefilled');
+
+ this.module._onModifyName()
+
+ this.module.field_url_input.val('http://example.com/undefined_file');
+ this.module._onFromWebBlur();
+ assert.equal(this.module.field_name.val(), 'prefilled');
+ });
+ });
+
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/image-upload.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/image-upload.spec.min.js
new file mode 100644
index 00000000..f03c83fe
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/image-upload.spec.min.js
@@ -0,0 +1,2 @@
+describe('ckan.modules.ImageUploadModule()',function(){var ImageUploadModule=ckan.module.registry['image-upload'];beforeEach(function(){this.el=document.createElement('div');this.sandbox=ckan.sandbox();this.module=new ImageUploadModule(this.el,{},this.sandbox);this.module.el.html(['
',' ',]);this.module.initialize();this.module.field_name=jQuery(' ',{type:'text'})});afterEach(function(){this.module.teardown();});describe('._onFromWeb()',function(){it('should change name when url changed',function(){this.module.field_url_input.val('http://example.com/some_image.png');this.module._onFromWebBlur();assert.equal(this.module.field_name.val(),'some_image.png');this.module.field_url_input.val('http://example.com/undefined_file');this.module._onFromWebBlur();assert.equal(this.module.field_name.val(),'undefined_file');});it('should ignore url changes if name was manualy changed',function(){this.module.field_url_input.val('http://example.com/some_image.png');this.module._onFromWebBlur();assert.equal(this.module.field_name.val(),'some_image.png');this.module._onModifyName();this.module.field_url_input.val('http://example.com/undefined_file');this.module._onFromWebBlur();assert.equal(this.module.field_name.val(),'some_image.png');});it('should ignore url changes if name was filled before',function(){this.module._nameIsDirty=true;this.module.field_name.val('prefilled');this.module.field_url_input.val('http://example.com/some_image.png');this.module._onFromWebBlur();assert.equal(this.module.field_name.val(),'prefilled');this.module.field_url_input.val('http://example.com/second_some_image.png');this.module._onFromWebBlur();assert.equal(this.module.field_name.val(),'prefilled');this.module._onModifyName()
+this.module.field_url_input.val('http://example.com/undefined_file');this.module._onFromWebBlur();assert.equal(this.module.field_name.val(),'prefilled');});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-form.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-form.spec.js
new file mode 100644
index 00000000..78975e32
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-form.spec.js
@@ -0,0 +1,74 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('ckan.modules.ResourceFormModule()', function () {
+ var ResourceFormModule = ckan.module.registry['resource-form'];
+
+ beforeEach(function () {
+ this.el = document.createElement('form');
+ this.sandbox = ckan.sandbox();
+ this.module = new ResourceFormModule(this.el, {}, this.sandbox);
+ });
+
+ afterEach(function () {
+ this.module.teardown();
+ });
+
+ describe('.initialize()', function () {
+ it('should subscribe to the "resource:uploaded" event', function () {
+ var target = sinon.stub(this.sandbox, 'subscribe');
+
+ this.module.initialize();
+
+ assert.called(target);
+ assert.calledWith(target, 'resource:uploaded', this.module._onResourceUploaded);
+
+ target.restore();
+ });
+ });
+
+ describe('.teardown()', function () {
+ it('should unsubscribe from the "resource:uploaded" event', function () {
+ var target = sinon.stub(this.sandbox, 'unsubscribe');
+
+ this.module.teardown();
+
+ assert.called(target);
+ assert.calledWith(target, 'resource:uploaded', this.module._onResourceUploaded);
+
+ target.restore();
+ });
+ });
+
+ describe('._onResourceUploaded()', function () {
+ beforeEach(function () {
+ this.module.el.html([
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ' ',
+ ' ',
+ ' '
+ ].join(''));
+
+ this.resource = {
+ text: 'text',
+ checkbox: "check",
+ radio: "radio2",
+ hidden: "hidden",
+ select: "option1"
+ };
+ });
+
+ it('should set the values on appropriate fields', function () {
+ var res = this.resource;
+
+ this.module._onResourceUploaded(res);
+
+ jQuery.each(this.module.el.serializeArray(), function (idx, field) {
+ assert.equal(field.value, res[field.name]);
+ });
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-form.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-form.spec.min.js
new file mode 100644
index 00000000..39c34e32
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-form.spec.min.js
@@ -0,0 +1 @@
+describe('ckan.modules.ResourceFormModule()',function(){var ResourceFormModule=ckan.module.registry['resource-form'];beforeEach(function(){this.el=document.createElement('form');this.sandbox=ckan.sandbox();this.module=new ResourceFormModule(this.el,{},this.sandbox);});afterEach(function(){this.module.teardown();});describe('.initialize()',function(){it('should subscribe to the "resource:uploaded" event',function(){var target=sinon.stub(this.sandbox,'subscribe');this.module.initialize();assert.called(target);assert.calledWith(target,'resource:uploaded',this.module._onResourceUploaded);target.restore();});});describe('.teardown()',function(){it('should unsubscribe from the "resource:uploaded" event',function(){var target=sinon.stub(this.sandbox,'unsubscribe');this.module.teardown();assert.called(target);assert.calledWith(target,'resource:uploaded',this.module._onResourceUploaded);target.restore();});});describe('._onResourceUploaded()',function(){beforeEach(function(){this.module.el.html([' ',' ',' ',' ',' ','',' ',' ',' '].join(''));this.resource={text:'text',checkbox:"check",radio:"radio2",hidden:"hidden",select:"option1"};});it('should set the values on appropriate fields',function(){var res=this.resource;this.module._onResourceUploaded(res);jQuery.each(this.module.el.serializeArray(),function(idx,field){assert.equal(field.value,res[field.name]);});});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-upload-field.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-upload-field.spec.js
new file mode 100644
index 00000000..dcf2ca06
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-upload-field.spec.js
@@ -0,0 +1,290 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('ckan.modules.ResourceUploadFieldModule()', function () {
+ var ResourceFileUploadModule = ckan.module.registry['resource-upload-field'];
+
+ beforeEach(function () {
+ jQuery.fn.fileupload = sinon.spy();
+
+ this.el = jQuery('');
+ this.sandbox = ckan.sandbox();
+ this.module = new ResourceFileUploadModule(this.el, {}, this.sandbox);
+ this.module.initialize();
+ });
+
+ afterEach(function () {
+ this.module.teardown();
+ });
+
+ describe('.initialize()', function () {
+ beforeEach(function () {
+ // Create un-initialised module.
+ this.module.teardown();
+ this.module = new ResourceFileUploadModule(this.el, {}, this.sandbox);
+ });
+
+ it('should create the #upload field', function () {
+ this.module.initialize();
+ assert.ok(typeof this.module.upload === 'object');
+ });
+
+ it('should append the upload field to the module element', function () {
+ this.module.initialize();
+
+ assert.ok(jQuery.contains(this.el[0], this.module.upload[0]));
+ });
+
+ it('should call .setupFileUpload()', function () {
+ var target = sinon.stub(this.module, 'setupFileUpload');
+
+ this.module.initialize();
+
+ assert.called(target);
+ });
+ });
+
+ describe('.setupFileUpload()', function () {
+ it('should set the label text on the form input', function () {
+ this.module.initialize();
+ this.module.setupFileUpload();
+
+ assert.equal(this.module.upload.find('label').text(), 'Upload a file');
+ });
+
+ it('should setup the file upload with relevant options', function () {
+ this.module.initialize();
+ this.module.setupFileUpload();
+
+ assert.called(jQuery.fn.fileupload);
+ assert.calledWith(jQuery.fn.fileupload, {
+ type: 'POST',
+ paramName: 'file',
+ forceIframeTransport: true, // Required for XDomain request.
+ replaceFileInput: true,
+ autoUpload: false,
+ add: this.module._onUploadAdd,
+ send: this.module._onUploadSend,
+ done: this.module._onUploadDone,
+ fail: this.module._onUploadFail,
+ always: this.module._onUploadComplete
+ });
+ });
+ });
+
+ describe('.loading(show)', function () {
+ it('should add a loading class to the upload element', function () {
+ this.module.loading();
+
+ assert.ok(this.module.upload.hasClass('loading'));
+ });
+
+ it('should remove the loading class if false is passed as an argument', function () {
+ this.module.upload.addClass('loading');
+ this.module.loading();
+
+ assert.ok(!this.module.upload.hasClass('loading'));
+ });
+ });
+
+ describe('.authenticate(key, data)', function () {
+ beforeEach(function () {
+ this.fakeThen = sinon.spy();
+ this.fakeProxy = sinon.stub(jQuery, 'proxy').returns('onsuccess');
+
+ this.target = sinon.stub(this.sandbox.client, 'getStorageAuth');
+ this.target.returns({
+ then: this.fakeThen
+ });
+ });
+
+ afterEach(function () {
+ jQuery.proxy.restore();
+ });
+
+ it('should request authentication for the upload', function () {
+ this.module.authenticate('test', {});
+ assert.called(this.target);
+ assert.calledWith(this.target, 'test');
+ });
+
+ it('should register success and error callbacks', function () {
+ this.module.authenticate('test', {});
+ assert.called(this.fakeThen);
+ assert.calledWith(this.fakeThen, 'onsuccess', this.module._onAuthError);
+ });
+
+ it('should save the key on the data object', function () {
+ var data = {};
+
+ this.module.authenticate('test', data);
+
+ assert.equal(data.key, 'test');
+ });
+ });
+
+ describe('.lookupMetadata(key, data)', function () {
+ beforeEach(function () {
+ this.fakeThen = sinon.spy();
+ this.fakeProxy = sinon.stub(jQuery, 'proxy').returns('onsuccess');
+
+ this.target = sinon.stub(this.sandbox.client, 'getStorageMetadata');
+ this.target.returns({
+ then: this.fakeThen
+ });
+ });
+
+ afterEach(function () {
+ jQuery.proxy.restore();
+ });
+
+ it('should request metadata for the upload key', function () {
+ this.module.lookupMetadata('test', {});
+ assert.called(this.target);
+ assert.calledWith(this.target, 'test');
+ });
+
+ it('should register success and error callbacks', function () {
+ this.module.lookupMetadata('test', {});
+ assert.called(this.fakeThen);
+ assert.calledWith(this.fakeThen, 'onsuccess', this.module._onMetadataError);
+ });
+ });
+
+ describe('.notify(message, type)', function () {
+ it('should call the sandbox.notify() method', function () {
+ var target = sinon.stub(this.sandbox, 'notify');
+
+ this.module.notify('this is an example message', 'info');
+
+ assert.called(target);
+ assert.calledWith(target, 'An Error Occurred', 'this is an example message', 'info');
+ });
+ });
+
+ describe('.generateKey(file)', function () {
+ it('should generate a unique filename prefixed with a timestamp', function () {
+ var now = new Date();
+ var date = jQuery.date.toISOString(now);
+ var clock = sinon.useFakeTimers(now.getTime());
+ var target = this.module.generateKey('this is my file.png');
+
+ assert.equal(target, date + '/this-is-my-file.png');
+
+ clock.restore();
+ });
+ });
+
+ describe('._onUploadAdd(event, data)', function () {
+ beforeEach(function () {
+ this.target = sinon.stub(this.module, 'authenticate');
+ sinon.stub(this.module, 'generateKey').returns('stubbed');
+ });
+
+ it('should authenticate the upload if a file is provided', function () {
+ var data = {files: [{name: 'my_file.jpg'}]};
+ this.module._onUploadAdd({}, data);
+
+ assert.called(this.target);
+ assert.calledWith(this.target, 'stubbed', data);
+ });
+
+ it('should not authenticate the upload if no file is provided', function () {
+ var data = {files: []};
+ this.module._onUploadAdd({}, data);
+
+ assert.notCalled(this.target);
+ });
+ });
+
+ describe('._onUploadSend()', function () {
+ it('should display the loading spinner', function () {
+ var target = sinon.stub(this.module, 'loading');
+ this.module._onUploadSend({}, {});
+
+ assert.called(target);
+ });
+ });
+
+ describe('._onUploadDone()', function () {
+ it('should request the metadata for the file', function () {
+ var target = sinon.stub(this.module, 'lookupMetadata');
+ this.module._onUploadDone({}, {result: {}});
+
+ assert.called(target);
+ });
+
+ it('should call the fail handler if the "result" key in the data is undefined', function () {
+ var target = sinon.stub(this.module, '_onUploadFail');
+ this.module._onUploadDone({}, {result: undefined});
+
+ assert.called(target);
+ });
+
+ it('should call the fail handler if the "result" object has an "error" key', function () {
+ var target = sinon.stub(this.module, '_onUploadFail');
+ this.module._onUploadDone({}, {result: {error: 'failed'}});
+
+ assert.called(target);
+ });
+ });
+
+ describe('._onUploadComplete()', function () {
+ it('should hide the loading spinner', function () {
+ var target = sinon.stub(this.module, 'loading');
+ this.module._onUploadComplete({}, {});
+
+ assert.called(target);
+ assert.calledWith(target, false);
+ });
+ });
+
+ describe('._onAuthSuccess()', function () {
+ beforeEach(function () {
+ this.target = {
+ submit: sinon.spy()
+ };
+
+ this.response = {
+ action: 'action',
+ fields: [{name: 'name', value: 'value'}]
+ };
+ });
+
+ it('should set the data url', function () {
+ this.module._onAuthSuccess(this.target, this.response);
+
+ assert.equal(this.target.url, this.response.action);
+ });
+
+ it('should set the additional form data', function () {
+ this.module._onAuthSuccess(this.target, this.response);
+
+ assert.deepEqual(this.target.formData, this.response.fields);
+ });
+
+ it('should merge the form data with the options', function () {
+ this.module.options.form.params = [{name: 'option', value: 'option'}];
+ this.module._onAuthSuccess(this.target, this.response);
+
+ assert.deepEqual(this.target.formData, [{name: 'option', value: 'option'}, {name: 'name', value: 'value'}]);
+ });
+
+ it('should call data.submit()', function () {
+ this.module._onAuthSuccess(this.target, this.response);
+ assert.called(this.target.submit);
+ });
+ });
+
+ describe('._onMetadataSuccess()', function () {
+ it('should publish the "resource:uploaded" event', function () {
+ var resource = {url: 'http://', name: 'My File'};
+ var target = sinon.stub(this.sandbox, 'publish');
+
+ sinon.stub(this.sandbox.client, 'convertStorageMetadataToResource').returns(resource);
+
+ this.module._onMetadataSuccess();
+
+ assert.called(target);
+ assert.calledWith(target, "resource:uploaded", resource);
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-upload-field.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-upload-field.spec.min.js
new file mode 100644
index 00000000..42a450c9
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/modules/resource-upload-field.spec.min.js
@@ -0,0 +1 @@
+describe('ckan.modules.ResourceUploadFieldModule()',function(){var ResourceFileUploadModule=ckan.module.registry['resource-upload-field'];beforeEach(function(){jQuery.fn.fileupload=sinon.spy();this.el=jQuery('');this.sandbox=ckan.sandbox();this.module=new ResourceFileUploadModule(this.el,{},this.sandbox);this.module.initialize();});afterEach(function(){this.module.teardown();});describe('.initialize()',function(){beforeEach(function(){this.module.teardown();this.module=new ResourceFileUploadModule(this.el,{},this.sandbox);});it('should create the #upload field',function(){this.module.initialize();assert.ok(typeof this.module.upload==='object');});it('should append the upload field to the module element',function(){this.module.initialize();assert.ok(jQuery.contains(this.el[0],this.module.upload[0]));});it('should call .setupFileUpload()',function(){var target=sinon.stub(this.module,'setupFileUpload');this.module.initialize();assert.called(target);});});describe('.setupFileUpload()',function(){it('should set the label text on the form input',function(){this.module.initialize();this.module.setupFileUpload();assert.equal(this.module.upload.find('label').text(),'Upload a file');});it('should setup the file upload with relevant options',function(){this.module.initialize();this.module.setupFileUpload();assert.called(jQuery.fn.fileupload);assert.calledWith(jQuery.fn.fileupload,{type:'POST',paramName:'file',forceIframeTransport:true,replaceFileInput:true,autoUpload:false,add:this.module._onUploadAdd,send:this.module._onUploadSend,done:this.module._onUploadDone,fail:this.module._onUploadFail,always:this.module._onUploadComplete});});});describe('.loading(show)',function(){it('should add a loading class to the upload element',function(){this.module.loading();assert.ok(this.module.upload.hasClass('loading'));});it('should remove the loading class if false is passed as an argument',function(){this.module.upload.addClass('loading');this.module.loading();assert.ok(!this.module.upload.hasClass('loading'));});});describe('.authenticate(key, data)',function(){beforeEach(function(){this.fakeThen=sinon.spy();this.fakeProxy=sinon.stub(jQuery,'proxy').returns('onsuccess');this.target=sinon.stub(this.sandbox.client,'getStorageAuth');this.target.returns({then:this.fakeThen});});afterEach(function(){jQuery.proxy.restore();});it('should request authentication for the upload',function(){this.module.authenticate('test',{});assert.called(this.target);assert.calledWith(this.target,'test');});it('should register success and error callbacks',function(){this.module.authenticate('test',{});assert.called(this.fakeThen);assert.calledWith(this.fakeThen,'onsuccess',this.module._onAuthError);});it('should save the key on the data object',function(){var data={};this.module.authenticate('test',data);assert.equal(data.key,'test');});});describe('.lookupMetadata(key, data)',function(){beforeEach(function(){this.fakeThen=sinon.spy();this.fakeProxy=sinon.stub(jQuery,'proxy').returns('onsuccess');this.target=sinon.stub(this.sandbox.client,'getStorageMetadata');this.target.returns({then:this.fakeThen});});afterEach(function(){jQuery.proxy.restore();});it('should request metadata for the upload key',function(){this.module.lookupMetadata('test',{});assert.called(this.target);assert.calledWith(this.target,'test');});it('should register success and error callbacks',function(){this.module.lookupMetadata('test',{});assert.called(this.fakeThen);assert.calledWith(this.fakeThen,'onsuccess',this.module._onMetadataError);});});describe('.notify(message, type)',function(){it('should call the sandbox.notify() method',function(){var target=sinon.stub(this.sandbox,'notify');this.module.notify('this is an example message','info');assert.called(target);assert.calledWith(target,'An Error Occurred','this is an example message','info');});});describe('.generateKey(file)',function(){it('should generate a unique filename prefixed with a timestamp',function(){var now=new Date();var date=jQuery.date.toISOString(now);var clock=sinon.useFakeTimers(now.getTime());var target=this.module.generateKey('this is my file.png');assert.equal(target,date+'/this-is-my-file.png');clock.restore();});});describe('._onUploadAdd(event, data)',function(){beforeEach(function(){this.target=sinon.stub(this.module,'authenticate');sinon.stub(this.module,'generateKey').returns('stubbed');});it('should authenticate the upload if a file is provided',function(){var data={files:[{name:'my_file.jpg'}]};this.module._onUploadAdd({},data);assert.called(this.target);assert.calledWith(this.target,'stubbed',data);});it('should not authenticate the upload if no file is provided',function(){var data={files:[]};this.module._onUploadAdd({},data);assert.notCalled(this.target);});});describe('._onUploadSend()',function(){it('should display the loading spinner',function(){var target=sinon.stub(this.module,'loading');this.module._onUploadSend({},{});assert.called(target);});});describe('._onUploadDone()',function(){it('should request the metadata for the file',function(){var target=sinon.stub(this.module,'lookupMetadata');this.module._onUploadDone({},{result:{}});assert.called(target);});it('should call the fail handler if the "result" key in the data is undefined',function(){var target=sinon.stub(this.module,'_onUploadFail');this.module._onUploadDone({},{result:undefined});assert.called(target);});it('should call the fail handler if the "result" object has an "error" key',function(){var target=sinon.stub(this.module,'_onUploadFail');this.module._onUploadDone({},{result:{error:'failed'}});assert.called(target);});});describe('._onUploadComplete()',function(){it('should hide the loading spinner',function(){var target=sinon.stub(this.module,'loading');this.module._onUploadComplete({},{});assert.called(target);assert.calledWith(target,false);});});describe('._onAuthSuccess()',function(){beforeEach(function(){this.target={submit:sinon.spy()};this.response={action:'action',fields:[{name:'name',value:'value'}]};});it('should set the data url',function(){this.module._onAuthSuccess(this.target,this.response);assert.equal(this.target.url,this.response.action);});it('should set the additional form data',function(){this.module._onAuthSuccess(this.target,this.response);assert.deepEqual(this.target.formData,this.response.fields);});it('should merge the form data with the options',function(){this.module.options.form.params=[{name:'option',value:'option'}];this.module._onAuthSuccess(this.target,this.response);assert.deepEqual(this.target.formData,[{name:'option',value:'option'},{name:'name',value:'value'}]);});it('should call data.submit()',function(){this.module._onAuthSuccess(this.target,this.response);assert.called(this.target.submit);});});describe('._onMetadataSuccess()',function(){it('should publish the "resource:uploaded" event',function(){var resource={url:'http://',name:'My File'};var target=sinon.stub(this.sandbox,'publish');sinon.stub(this.sandbox.client,'convertStorageMetadataToResource').returns(resource);this.module._onMetadataSuccess();assert.called(target);assert.calledWith(target,"resource:uploaded",resource);});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.date-helpers.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.date-helpers.spec.js
new file mode 100644
index 00000000..6e3a81c0
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.date-helpers.spec.js
@@ -0,0 +1,37 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('jQuery.date', function () {
+ beforeEach(function () {
+ this.now = new Date();
+ this.now.setTime(0);
+
+ this.clock = sinon.useFakeTimers(this.now.getTime());
+ });
+
+ afterEach(function () {
+ this.clock.restore();
+ });
+
+ describe('jQuery.date.format()', function () {
+ it('should format the date based on the string provided', function () {
+ var target = jQuery.date.format('yyyy-MM-dd', this.now);
+ assert.equal(target, '1970-01-01');
+ });
+
+ it('should use the current time if none provided', function () {
+ var target = jQuery.date.format('yyyy/MM/dd');
+ assert.equal(target, '1970/01/01');
+ });
+ });
+
+ describe('jQuery.date.toISOString()', function () {
+ it('should output an ISO8601 compatible string', function () {
+ var target = jQuery.date.toISOString(this.now);
+ assert.equal(target, '1970-01-01T00:00:00.000Z');
+ });
+
+ it('should use the current time if none provided', function () {
+ var target = jQuery.date.toISOString();
+ assert.equal(target, '1970-01-01T00:00:00.000Z');
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.date-helpers.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.date-helpers.spec.min.js
new file mode 100644
index 00000000..39e200ba
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.date-helpers.spec.min.js
@@ -0,0 +1 @@
+describe('jQuery.date',function(){beforeEach(function(){this.now=new Date();this.now.setTime(0);this.clock=sinon.useFakeTimers(this.now.getTime());});afterEach(function(){this.clock.restore();});describe('jQuery.date.format()',function(){it('should format the date based on the string provided',function(){var target=jQuery.date.format('yyyy-MM-dd',this.now);assert.equal(target,'1970-01-01');});it('should use the current time if none provided',function(){var target=jQuery.date.format('yyyy/MM/dd');assert.equal(target,'1970/01/01');});});describe('jQuery.date.toISOString()',function(){it('should output an ISO8601 compatible string',function(){var target=jQuery.date.toISOString(this.now);assert.equal(target,'1970-01-01T00:00:00.000Z');});it('should use the current time if none provided',function(){var target=jQuery.date.toISOString();assert.equal(target,'1970-01-01T00:00:00.000Z');});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.form-warning.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.form-warning.spec.js
new file mode 100644
index 00000000..406ec36b
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.form-warning.spec.js
@@ -0,0 +1,42 @@
+describe('jQuery.incompleteFormWarning()', function () {
+ beforeEach(function () {
+ this.el = jQuery('').appendTo(this.fixture);
+ this.el.on('submit', false);
+
+ this.input1 = jQuery(' ').appendTo(this.el);
+ this.input2 = jQuery(' ').appendTo(this.el);
+
+ this.el.incompleteFormWarning('my message');
+
+ this.on = sinon.stub(jQuery.fn, 'on');
+ this.off = sinon.stub(jQuery.fn, 'off');
+ });
+
+ afterEach(function () {
+ this.on.restore();
+ this.off.restore();
+ });
+
+ it('should bind a beforeunload event when the form changes', function () {
+ this.input1.val('c');
+ this.el.change();
+
+ assert.called(this.on);
+ });
+
+ it('should unbind a beforeunload event when a form returns to the original state', function () {
+ this.input1.val('c');
+ this.el.change();
+
+ this.input1.val('a');
+ this.el.change();
+
+ assert.called(this.off);
+ });
+
+ it('should unbind the beforeunload event when the form is submitted', function () {
+ this.el.submit();
+
+ assert.called(this.off);
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.form-warning.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.form-warning.spec.min.js
new file mode 100644
index 00000000..5e18e1cb
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.form-warning.spec.min.js
@@ -0,0 +1 @@
+describe('jQuery.incompleteFormWarning()',function(){beforeEach(function(){this.el=jQuery('').appendTo(this.fixture);this.el.on('submit',false);this.input1=jQuery(' ').appendTo(this.el);this.input2=jQuery(' ').appendTo(this.el);this.el.incompleteFormWarning('my message');this.on=sinon.stub(jQuery.fn,'on');this.off=sinon.stub(jQuery.fn,'off');});afterEach(function(){this.on.restore();this.off.restore();});it('should bind a beforeunload event when the form changes',function(){this.input1.val('c');this.el.change();assert.called(this.on);});it('should unbind a beforeunload event when a form returns to the original state',function(){this.input1.val('c');this.el.change();this.input1.val('a');this.el.change();assert.called(this.off);});it('should unbind the beforeunload event when the form is submitted',function(){this.el.submit();assert.called(this.off);});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.inherit.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.inherit.spec.js
new file mode 100644
index 00000000..d84bcba9
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.inherit.spec.js
@@ -0,0 +1,46 @@
+/*globals describe beforeEach afterEach it assert sinon ckan jQuery */
+describe('jQuery.inherit()', function () {
+ beforeEach(function () {
+ this.MyClass = function MyClass() {};
+ this.MyClass.static = function () {};
+ this.MyClass.prototype.method = function () {};
+ });
+
+ it('should create a subclass of the constructor provided', function () {
+ var target = new (jQuery.inherit(this.MyClass))();
+ assert.isTrue(target instanceof this.MyClass);
+ });
+
+ it('should set the childs prototype object', function () {
+ var target = new (jQuery.inherit(this.MyClass))();
+ assert.isFunction(target.method);
+ });
+
+ it('should copy over the childs static properties', function () {
+ var Target = jQuery.inherit(this.MyClass);
+ assert.isFunction(Target.static);
+ });
+
+ it('should allow instance properties to be overridden', function () {
+ function method() {}
+
+ var target = new (jQuery.inherit(this.MyClass, {method: method}))();
+ assert.equal(target.method, method);
+ });
+
+ it('should allow static properties to be overridden', function () {
+ function staticmethod() {}
+
+ var Target = jQuery.inherit(this.MyClass, {}, {static: staticmethod});
+ assert.equal(Target.static, staticmethod);
+ });
+
+ it('should allow a custom constructor to be provided', function () {
+ var MyConstructor = sinon.spy();
+ var Target = jQuery.inherit(this.MyClass, {constructor: MyConstructor});
+
+ new Target();
+
+ sinon.assert.called(MyConstructor);
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.inherit.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.inherit.spec.min.js
new file mode 100644
index 00000000..6482dfe0
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.inherit.spec.min.js
@@ -0,0 +1,3 @@
+describe('jQuery.inherit()',function(){beforeEach(function(){this.MyClass=function MyClass(){};this.MyClass.static=function(){};this.MyClass.prototype.method=function(){};});it('should create a subclass of the constructor provided',function(){var target=new(jQuery.inherit(this.MyClass))();assert.isTrue(target instanceof this.MyClass);});it('should set the childs prototype object',function(){var target=new(jQuery.inherit(this.MyClass))();assert.isFunction(target.method);});it('should copy over the childs static properties',function(){var Target=jQuery.inherit(this.MyClass);assert.isFunction(Target.static);});it('should allow instance properties to be overridden',function(){function method(){}
+var target=new(jQuery.inherit(this.MyClass,{method:method}))();assert.equal(target.method,method);});it('should allow static properties to be overridden',function(){function staticmethod(){}
+var Target=jQuery.inherit(this.MyClass,{},{static:staticmethod});assert.equal(Target.static,staticmethod);});it('should allow a custom constructor to be provided',function(){var MyConstructor=sinon.spy();var Target=jQuery.inherit(this.MyClass,{constructor:MyConstructor});new Target();sinon.assert.called(MyConstructor);});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.proxy-all.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.proxy-all.spec.js
new file mode 100644
index 00000000..56dd373c
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.proxy-all.spec.js
@@ -0,0 +1,60 @@
+/*globals describe it beforeEach afterEach jQuery sinon assert */
+describe('jQuery.proxyAll(obj, args...)', function () {
+ beforeEach(function () {
+ this.proxy = sinon.stub(jQuery, 'proxy').returns(function proxied() {});
+ this.target = {
+ prop1: '',
+ method1: function method1() {},
+ method2: function method2() {},
+ method3: function method3() {}
+ };
+
+ this.cloned = jQuery.extend({}, this.target);
+ });
+
+ afterEach(function () {
+ this.proxy.restore();
+ });
+
+ it('should bind the methods provided to the object', function () {
+ jQuery.proxyAll(this.target, 'method1', 'method2');
+
+ assert.called(this.proxy);
+
+ assert.calledWith(this.proxy, this.cloned.method1, this.target);
+ assert.calledWith(this.proxy, this.cloned.method2, this.target);
+ assert.neverCalledWith(this.proxy, this.cloned.method3, this.target);
+ });
+
+ it('should allow regular expressions to be provided', function () {
+ jQuery.proxyAll(this.target, /method[1-2]/);
+
+ assert.called(this.proxy);
+
+ assert.calledWith(this.proxy, this.cloned.method1, this.target);
+ assert.calledWith(this.proxy, this.cloned.method2, this.target);
+ assert.neverCalledWith(this.proxy, this.cloned.method3, this.target);
+ });
+
+ it('should skip properties that are not functions', function () {
+ jQuery.proxyAll(this.target, 'prop1');
+ assert.notCalled(this.proxy);
+ });
+
+ it('should not bind function more than once', function () {
+ jQuery.proxyAll(this.target, 'method1');
+ jQuery.proxyAll(this.target, 'method1');
+
+ assert.calledOnce(this.proxy);
+ });
+
+ it('should not bind function more than once if the method name is passed twice', function () {
+ jQuery.proxyAll(this.target, 'method1', 'method1');
+
+ assert.calledOnce(this.proxy);
+ });
+
+ it('should return the first argument', function () {
+ assert.equal(jQuery.proxyAll(this.target), this.target);
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.proxy-all.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.proxy-all.spec.min.js
new file mode 100644
index 00000000..7f25f50f
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.proxy-all.spec.min.js
@@ -0,0 +1 @@
+describe('jQuery.proxyAll(obj, args...)',function(){beforeEach(function(){this.proxy=sinon.stub(jQuery,'proxy').returns(function proxied(){});this.target={prop1:'',method1:function method1(){},method2:function method2(){},method3:function method3(){}};this.cloned=jQuery.extend({},this.target);});afterEach(function(){this.proxy.restore();});it('should bind the methods provided to the object',function(){jQuery.proxyAll(this.target,'method1','method2');assert.called(this.proxy);assert.calledWith(this.proxy,this.cloned.method1,this.target);assert.calledWith(this.proxy,this.cloned.method2,this.target);assert.neverCalledWith(this.proxy,this.cloned.method3,this.target);});it('should allow regular expressions to be provided',function(){jQuery.proxyAll(this.target,/method[1-2]/);assert.called(this.proxy);assert.calledWith(this.proxy,this.cloned.method1,this.target);assert.calledWith(this.proxy,this.cloned.method2,this.target);assert.neverCalledWith(this.proxy,this.cloned.method3,this.target);});it('should skip properties that are not functions',function(){jQuery.proxyAll(this.target,'prop1');assert.notCalled(this.proxy);});it('should not bind function more than once',function(){jQuery.proxyAll(this.target,'method1');jQuery.proxyAll(this.target,'method1');assert.calledOnce(this.proxy);});it('should not bind function more than once if the method name is passed twice',function(){jQuery.proxyAll(this.target,'method1','method1');assert.calledOnce(this.proxy);});it('should return the first argument',function(){assert.equal(jQuery.proxyAll(this.target),this.target);});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug-preview.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug-preview.spec.js
new file mode 100644
index 00000000..2ac9ae88
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug-preview.spec.js
@@ -0,0 +1,65 @@
+/*globals describe it assert jQuery*/
+describe('jQuery.fn.slugPreview()', function () {
+ beforeEach(function () {
+ this.element = jQuery('
');
+ });
+
+ it('should return the preview element', function () {
+ var target = this.element.slugPreview();
+ assert.ok(target.hasClass('slug-preview'));
+ });
+
+ it('should restore the stack when .end() is called', function () {
+ var target = this.element.slugPreview();
+ assert.ok(target.end() === this.element);
+ });
+
+ it('should allow a prefix to be provided', function () {
+ var target = this.element.slugPreview({prefix: 'prefix'});
+ assert.equal(target.find('.slug-preview-prefix').text(), 'prefix');
+ });
+
+ it('should allow a placeholder to be provided', function () {
+ var target = this.element.slugPreview({placeholder: 'placeholder'});
+ assert.equal(target.find('.slug-preview-value').text(), 'placeholder');
+ });
+
+ it('should allow translations for strings to be provided', function () {
+ var target = this.element.slugPreview({
+ i18n: {'Edit': 'translated'}
+ });
+ assert.equal(target.find('button').text(), 'translated');
+ });
+
+ it('should set preview value to the initial value of the input', function () {
+ var input = this.element.find('input').val('initial');
+ var target = this.element.slugPreview();
+
+ assert.equal(target.find('.slug-preview-value').text(), 'initial');
+ });
+
+ it('should update the preview value when the target input changes', function () {
+ var target = this.element.slugPreview();
+ var input = this.element.find('input').val('initial');
+
+ input.val('updated').change();
+ assert.equal(target.find('.slug-preview-value').text(), 'updated');
+ });
+
+ it('should hide the original element', function () {
+ var target = this.element.slugPreview();
+ assert.ok(this.element.css('display') === 'none');
+ });
+
+ it('should show the original element when Edit is clicked', function () {
+ var target = this.element.slugPreview();
+ target.find('button').click();
+ assert.ok(this.element.css('display') === '');
+ });
+
+ it('should hide the preview element when Edit is clicked', function () {
+ var target = this.element.slugPreview();
+ target.find('button').click();
+ assert.ok(target.css('display') === 'none');
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug-preview.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug-preview.spec.min.js
new file mode 100644
index 00000000..1452d511
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug-preview.spec.min.js
@@ -0,0 +1 @@
+describe('jQuery.fn.slugPreview()',function(){beforeEach(function(){this.element=jQuery('
');});it('should return the preview element',function(){var target=this.element.slugPreview();assert.ok(target.hasClass('slug-preview'));});it('should restore the stack when .end() is called',function(){var target=this.element.slugPreview();assert.ok(target.end()===this.element);});it('should allow a prefix to be provided',function(){var target=this.element.slugPreview({prefix:'prefix'});assert.equal(target.find('.slug-preview-prefix').text(),'prefix');});it('should allow a placeholder to be provided',function(){var target=this.element.slugPreview({placeholder:'placeholder'});assert.equal(target.find('.slug-preview-value').text(),'placeholder');});it('should allow translations for strings to be provided',function(){var target=this.element.slugPreview({i18n:{'Edit':'translated'}});assert.equal(target.find('button').text(),'translated');});it('should set preview value to the initial value of the input',function(){var input=this.element.find('input').val('initial');var target=this.element.slugPreview();assert.equal(target.find('.slug-preview-value').text(),'initial');});it('should update the preview value when the target input changes',function(){var target=this.element.slugPreview();var input=this.element.find('input').val('initial');input.val('updated').change();assert.equal(target.find('.slug-preview-value').text(),'updated');});it('should hide the original element',function(){var target=this.element.slugPreview();assert.ok(this.element.css('display')==='none');});it('should show the original element when Edit is clicked',function(){var target=this.element.slugPreview();target.find('button').click();assert.ok(this.element.css('display')==='');});it('should hide the preview element when Edit is clicked',function(){var target=this.element.slugPreview();target.find('button').click();assert.ok(target.css('display')==='none');});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug.spec.js
new file mode 100644
index 00000000..cc2555f8
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug.spec.js
@@ -0,0 +1,34 @@
+/*globals beforeEach describe it assert jQuery*/
+describe('jQuery.fn.slug()', function () {
+ beforeEach(function () {
+ this.input = jQuery(' ').slug();
+ this.fixture.append(this.input);
+ });
+
+ it('should slugify and append the pressed key', function () {
+ var e = jQuery.Event('keypress', {charCode: 97 /* a */});
+ this.input.trigger(e);
+
+ assert.equal(this.input.val(), 'a', 'append an "a"');
+
+ e = jQuery.Event('keypress', {charCode: 38 /* & */});
+ this.input.trigger(e);
+
+ assert.equal(this.input.val(), 'a-', 'append an "-"');
+ });
+
+ it('should do nothing if a non character key is pressed', function () {
+ var e = jQuery.Event('keypress', {charCode: 0});
+ this.input.val('some other string').trigger(e);
+
+ assert.equal(this.input.val(), 'some other string');
+ });
+
+ it('should slugify the input contents on "blur" and "change" events', function () {
+ this.input.val('apples & pears').trigger(jQuery.Event('blur'));
+ assert.equal(this.input.val(), 'apples-pears', 'on blur');
+
+ this.input.val('apples & pears').trigger(jQuery.Event('change'));
+ assert.equal(this.input.val(), 'apples-pears', 'on change');
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug.spec.min.js
new file mode 100644
index 00000000..c0825479
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.slug.spec.min.js
@@ -0,0 +1 @@
+describe('jQuery.fn.slug()',function(){beforeEach(function(){this.input=jQuery(' ').slug();this.fixture.append(this.input);});it('should slugify and append the pressed key',function(){var e=jQuery.Event('keypress',{charCode:97});this.input.trigger(e);assert.equal(this.input.val(),'a','append an "a"');e=jQuery.Event('keypress',{charCode:38});this.input.trigger(e);assert.equal(this.input.val(),'a-','append an "-"');});it('should do nothing if a non character key is pressed',function(){var e=jQuery.Event('keypress',{charCode:0});this.input.val('some other string').trigger(e);assert.equal(this.input.val(),'some other string');});it('should slugify the input contents on "blur" and "change" events',function(){this.input.val('apples & pears').trigger(jQuery.Event('blur'));assert.equal(this.input.val(),'apples-pears','on blur');this.input.val('apples & pears').trigger(jQuery.Event('change'));assert.equal(this.input.val(),'apples-pears','on change');});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.url-helpers.spec.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.url-helpers.spec.js
new file mode 100644
index 00000000..7737cac2
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.url-helpers.spec.js
@@ -0,0 +1,52 @@
+/*globals describe it assert jQuery*/
+describe('jQuery.url', function () {
+ describe('.escape()', function () {
+ it('should escape special characters', function () {
+ var target = jQuery.url.escape('&<>=?#/');
+ assert.equal(target, '%26%3C%3E%3D%3F%23%2F');
+ });
+
+ it('should convert spaces to + rather than %20', function () {
+ var target = jQuery.url.escape(' ');
+ assert.equal(target, '+');
+ });
+ });
+
+ describe('.slugify()', function () {
+ it('should replace spaces with hyphens', function () {
+ var target = jQuery.url.slugify('apples and pears');
+ assert.equal(target, 'apples-and-pears');
+ });
+
+ it('should lowecase all characters', function () {
+ var target = jQuery.url.slugify('APPLES AND PEARS');
+ assert.equal(target, 'apples-and-pears');
+ });
+
+ it('should convert unknown characters to hyphens', function () {
+ var target = jQuery.url.slugify('apples & pears');
+ assert.equal(target, 'apples-pears');
+ });
+
+ it('should nomalise hyphens', function () {
+ var target = jQuery.url.slugify('apples---pears');
+ assert.equal(target, 'apples-pears', 'remove duplicate hyphens');
+
+ target = jQuery.url.slugify('--apples-pears');
+ assert.equal(target, 'apples-pears', 'strip preceding hyphens');
+
+ target = jQuery.url.slugify('apples-pears--');
+ assert.equal(target, 'apples-pears', 'strip trailing hyphens');
+ });
+
+ it('should try and asciify unicode characters', function () {
+ var target = jQuery.url.slugify('éåøç');
+ assert.equal(target, 'eaoc');
+ });
+
+ it('should allow underscore characters', function() {
+ var target = jQuery.url.slugify('apples_pears');
+ assert.equal(target, 'apples_pears');
+ });
+ });
+});
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.url-helpers.spec.min.js b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.url-helpers.spec.min.js
new file mode 100644
index 00000000..a800dfe3
--- /dev/null
+++ b/venv/lib/python2.7/site-packages/ckan/public/base/test/spec/plugins/jquery.url-helpers.spec.min.js
@@ -0,0 +1 @@
+describe('jQuery.url',function(){describe('.escape()',function(){it('should escape special characters',function(){var target=jQuery.url.escape('&<>=?#/');assert.equal(target,'%26%3C%3E%3D%3F%23%2F');});it('should convert spaces to + rather than %20',function(){var target=jQuery.url.escape(' ');assert.equal(target,'+');});});describe('.slugify()',function(){it('should replace spaces with hyphens',function(){var target=jQuery.url.slugify('apples and pears');assert.equal(target,'apples-and-pears');});it('should lowecase all characters',function(){var target=jQuery.url.slugify('APPLES AND PEARS');assert.equal(target,'apples-and-pears');});it('should convert unknown characters to hyphens',function(){var target=jQuery.url.slugify('apples & pears');assert.equal(target,'apples-pears');});it('should nomalise hyphens',function(){var target=jQuery.url.slugify('apples---pears');assert.equal(target,'apples-pears','remove duplicate hyphens');target=jQuery.url.slugify('--apples-pears');assert.equal(target,'apples-pears','strip preceding hyphens');target=jQuery.url.slugify('apples-pears--');assert.equal(target,'apples-pears','strip trailing hyphens');});it('should try and asciify unicode characters',function(){var target=jQuery.url.slugify('éåøç');assert.equal(target,'eaoc');});it('should allow underscore characters',function(){var target=jQuery.url.slugify('apples_pears');assert.equal(target,'apples_pears');});});});
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot b/venv/lib/python2.7/site-packages/ckan/public/base/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..b93a4953fff68df523aa7656497ee339d6026d64
GIT binary patch
literal 20127
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/venv/lib/python2.7/site-packages/ckan/public/base/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf b/venv/lib/python2.7/site-packages/ckan/public/base/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..1413fc609ab6f21774de0cb7e01360095584f65b
GIT binary patch
literal 45404
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