├── client ├── code │ ├── shared │ ├── template │ ├── namespace.coffee │ ├── view │ │ ├── people-pack.coffee │ │ ├── fourohfour.coffee │ │ ├── home.coffee │ │ ├── thankyou.coffee │ │ ├── dataset │ │ │ ├── deleted.coffee │ │ │ ├── tile.coffee │ │ │ └── row.coffee │ │ ├── tool │ │ │ ├── list-header.coffee │ │ │ └── list.coffee │ │ ├── terms.coffee │ │ ├── profile │ │ │ ├── set-password.coffee │ │ │ ├── reset-password.coffee │ │ │ └── create-profile.coffee │ │ ├── docs.coffee │ │ ├── nav.coffee │ │ ├── error.coffee │ │ ├── subscribe.coffee │ │ └── sign-up.coffee │ ├── model │ │ ├── boxable.coffee │ │ ├── tool.coffee │ │ ├── view.coffee │ │ └── user.coffee │ ├── util.coffee │ └── app.coffee └── template │ ├── index.eco │ ├── dataset-tile-deleted.eco │ ├── dataset-row-deleted.eco │ ├── fourohfour.eco │ ├── subscribe.eco │ ├── subnav.eco │ ├── help-upload-and-summarise.eco │ ├── modal-upgrade.eco │ ├── helpnav.eco │ ├── modal-downgrade.eco │ ├── signupnav.eco │ ├── reset-password.eco │ ├── thankyou.eco │ ├── view-tile.eco │ ├── set-password.eco │ ├── dataset-views.eco │ ├── subnav-home.eco │ ├── modal-ssh.eco │ ├── modal-api-endpoints.eco │ ├── tool-tile.eco │ ├── toolbar-tile.eco │ ├── dataset-tile.eco │ ├── help-home.eco │ ├── subnav-toolbar.eco │ ├── modal-add-ssh.eco │ ├── dataset-row.eco │ ├── sign-up.eco │ ├── create-profile.eco │ ├── help-whats-new.eco │ └── nav.eco ├── .envrc ├── server.js ├── Dockerfile ├── shared ├── image │ ├── icon.png │ ├── logos.gif │ ├── avatar.png │ ├── favicon.ico │ ├── team_ed.png │ ├── avatar-rob.png │ ├── icon-cloud.png │ ├── icon-cross.png │ ├── micropig.jpg │ ├── team_aidan.png │ ├── team_aine.jpg │ ├── team_chris.png │ ├── team_david.png │ ├── team_ian.png │ ├── team_jane.png │ ├── team_paul.png │ ├── team_peter.png │ ├── header-logo.png │ ├── icon-rename.png │ ├── qc-logo-left.png │ ├── team_dragon.jpg │ ├── team_dragon.png │ ├── team_francis.png │ ├── team_julian.png │ ├── team_zarino.png │ ├── tool-loader.gif │ ├── toolbar-more.png │ ├── exclamation-red.png │ ├── icon-settings.png │ ├── icon-terminal.png │ ├── loader-btn-info.gif │ ├── loader-btn-link.gif │ ├── tool-icon-code.png │ ├── tool-icon-map.png │ ├── tool-icon-sql.png │ ├── tool-icon-test.png │ ├── tractor-500x320.png │ ├── chooser-icon-24px.png │ ├── header-highlight.png │ ├── icon-input-search.png │ ├── loader-btn-danger.gif │ ├── tile-options-grey.png │ ├── tool-icon-classic.png │ ├── tool-icon-events.png │ ├── tool-icon-lastfm.png │ ├── tool-icon-network.png │ ├── tool-icon-report.png │ ├── tool-icon-twitter.png │ ├── dataset-tools-toggle.png │ ├── loader-btn-default.gif │ ├── loader-btn-inverse.gif │ ├── loader-btn-primary.gif │ ├── loader-btn-success.gif │ ├── loader-btn-warning.gif │ ├── loader-input-search.gif │ ├── quickcode-rough-logo.png │ ├── tile-options-white.png │ ├── tool-icon-data-table.png │ ├── tool-icon-summarise.png │ ├── toolbar-scroll-left.png │ ├── toolbar-scroll-right.png │ ├── tractor-metro-tile.png │ ├── toolbar-triangle-mask.png │ ├── screenshots │ │ ├── tool-chooser.png │ │ ├── twitter-auth.png │ │ ├── twitter-toolbar.png │ │ ├── create-new-dataset.png │ │ ├── importer-chooser.png │ │ ├── twitter-download.png │ │ ├── twitter-schedule.png │ │ ├── twitter-table-view.png │ │ ├── twitter-search-term.png │ │ ├── code-in-browser-rename.png │ │ ├── code-in-browser-toolbar.png │ │ ├── code-your-own-tool-ssh.png │ │ ├── code-in-browser-language.png │ │ ├── code-your-own-tool-rename.png │ │ ├── code-your-own-tool-finished.png │ │ └── default-dataset-index-html.png │ ├── tool-icon-linkedin-groups.png │ ├── tool-icon-linkedin-people.png │ ├── tool-icon-notifications.png │ ├── tool-icon-linkedin-companies.png │ └── tool-icon-spreadsheet-upload.png ├── vendor │ ├── js │ │ ├── lang-go.js │ │ ├── lang-ml.js │ │ ├── lang-vb.js │ │ ├── lang-lua.js │ │ ├── lang-sql.js │ │ ├── lang-tex.js │ │ ├── lang-vhdl.js │ │ ├── lang-wiki.js │ │ ├── lang-apollo.js │ │ ├── lang-scala.js │ │ ├── ZeroClipboard.swf │ │ ├── lang-proto.js │ │ ├── lang-yaml.js │ │ ├── lang-hs.js │ │ ├── lang-lisp.js │ │ ├── lang-css.js │ │ ├── lang-n.js │ │ ├── lang-clj.js │ │ ├── jquery.tinysort.charorder.min.js │ │ ├── jquery.cookie.js │ │ └── jquery.tinysort.min.js │ ├── image │ │ ├── glyphicons-halflings.png │ │ └── glyphicons-halflings-white.png │ └── style │ │ └── prettify.css ├── style │ └── ie.css ├── sitemap.txt ├── template │ ├── termsfeed.rss │ └── 50x.html └── code │ ├── views.coffee │ └── page-titles.coffee ├── Procfile ├── tang.hook ├── .ssh └── config ├── docker ├── populate-node-modules.sh ├── custard-data-image │ └── Dockerfile ├── Dockerfile ├── start.sh └── run.sh ├── cron └── find-unsubscribed.sh ├── test ├── mocha.opts ├── unit │ ├── model │ │ ├── plan.coffee │ │ ├── view.coffee │ │ ├── box.coffee │ │ └── handle_error.coffee │ ├── setup_teardown.coffee │ ├── view │ │ ├── error-bar.coffee │ │ └── tile.coffee │ ├── helper.coffee │ └── index.coffee ├── cleaner.coffee └── integration │ ├── home_nologin.coffee │ ├── errors.coffee │ ├── setup_teardown.coffee │ ├── dataset_limit.coffee │ ├── terms.coffee │ ├── tool_privacy.coffee │ ├── signup.coffee │ ├── view.coffee │ ├── new_dataset.coffee │ ├── new_view.coffee │ ├── helper.coffee │ ├── create_profile.coffee │ └── ssh_platform_detect.coffee ├── pre-commit.sh ├── bin ├── mongo-timestamp ├── mongurl ├── livemongo ├── journalistDatasets.js ├── custard-munin ├── contact ├── whbox └── find-unsubscribed.coffee ├── .gitignore ├── docker-compose.yml ├── migration ├── update_tools.js ├── 090addMeToMe ├── 100addToBeDeleted ├── 050addBoxServers ├── 2013-08-symlink-migration │ ├── cobalt_symlink │ ├── should-migrate.sh │ └── generate-migrate-scripts.sh ├── 080addDatasetsBoxJSON ├── 060addDatasetServers ├── 040addRecurlyAccounts ├── checkServerConsistency ├── 010camelCase ├── 020grandfatherPlan └── 070addBoxJSON ├── util ├── listAllUserBoxes ├── generateShadowLines ├── generatePasswdLines ├── listUsersOnPlan ├── transferBoxData.sh ├── listBoxServersOfDir ├── changeUserPlan ├── addUnixUser.sh └── listGitURLsOfDir ├── server ├── code │ ├── model │ │ ├── plan.coffee │ │ ├── token.coffee │ │ ├── subscription.coffee │ │ ├── base.coffee │ │ └── tool.coffee │ ├── lib │ │ ├── sign.coffee │ │ └── email.coffee │ └── plans.json └── template │ ├── base_footer.html │ ├── not_found.html │ ├── base_header.html │ └── login.html ├── Cakefile ├── package.json └── activate /client/code/shared: -------------------------------------------------------------------------------- 1 | ../../shared -------------------------------------------------------------------------------- /client/code/template: -------------------------------------------------------------------------------- 1 | ../template -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | source_up 2 | source ./activate -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('./server/js'); 2 | -------------------------------------------------------------------------------- /client/template/index.eco: -------------------------------------------------------------------------------- 1 | #= require_tree . 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:0.10-onbuild 2 | EXPOSE 3001 3 | -------------------------------------------------------------------------------- /shared/image/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/icon.png -------------------------------------------------------------------------------- /shared/image/logos.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/logos.gif -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bash -c '. ./activate && cake dev' 2 | selenium: bash -c '. ./activate && cake se' 3 | -------------------------------------------------------------------------------- /shared/image/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/avatar.png -------------------------------------------------------------------------------- /shared/image/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/favicon.ico -------------------------------------------------------------------------------- /shared/image/team_ed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_ed.png -------------------------------------------------------------------------------- /tang.hook: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | echo "Hello, world" 4 | 5 | cd docker 6 | 7 | time ./run.sh 8 | 9 | -------------------------------------------------------------------------------- /shared/image/avatar-rob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/avatar-rob.png -------------------------------------------------------------------------------- /shared/image/icon-cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/icon-cloud.png -------------------------------------------------------------------------------- /shared/image/icon-cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/icon-cross.png -------------------------------------------------------------------------------- /shared/image/micropig.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/micropig.jpg -------------------------------------------------------------------------------- /shared/image/team_aidan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_aidan.png -------------------------------------------------------------------------------- /shared/image/team_aine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_aine.jpg -------------------------------------------------------------------------------- /shared/image/team_chris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_chris.png -------------------------------------------------------------------------------- /shared/image/team_david.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_david.png -------------------------------------------------------------------------------- /shared/image/team_ian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_ian.png -------------------------------------------------------------------------------- /shared/image/team_jane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_jane.png -------------------------------------------------------------------------------- /shared/image/team_paul.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_paul.png -------------------------------------------------------------------------------- /shared/image/team_peter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_peter.png -------------------------------------------------------------------------------- /shared/vendor/js/lang-go.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-go.js -------------------------------------------------------------------------------- /shared/vendor/js/lang-ml.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-ml.js -------------------------------------------------------------------------------- /shared/vendor/js/lang-vb.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-vb.js -------------------------------------------------------------------------------- /shared/image/header-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/header-logo.png -------------------------------------------------------------------------------- /shared/image/icon-rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/icon-rename.png -------------------------------------------------------------------------------- /shared/image/qc-logo-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/qc-logo-left.png -------------------------------------------------------------------------------- /shared/image/team_dragon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_dragon.jpg -------------------------------------------------------------------------------- /shared/image/team_dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_dragon.png -------------------------------------------------------------------------------- /shared/image/team_francis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_francis.png -------------------------------------------------------------------------------- /shared/image/team_julian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_julian.png -------------------------------------------------------------------------------- /shared/image/team_zarino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/team_zarino.png -------------------------------------------------------------------------------- /shared/image/tool-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-loader.gif -------------------------------------------------------------------------------- /shared/image/toolbar-more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/toolbar-more.png -------------------------------------------------------------------------------- /shared/vendor/js/lang-lua.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-lua.js -------------------------------------------------------------------------------- /shared/vendor/js/lang-sql.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-sql.js -------------------------------------------------------------------------------- /shared/vendor/js/lang-tex.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-tex.js -------------------------------------------------------------------------------- /shared/vendor/js/lang-vhdl.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-vhdl.js -------------------------------------------------------------------------------- /shared/vendor/js/lang-wiki.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-wiki.js -------------------------------------------------------------------------------- /client/template/dataset-tile-deleted.eco: -------------------------------------------------------------------------------- 1 |

Dataset deleted

2 | -------------------------------------------------------------------------------- /shared/image/exclamation-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/exclamation-red.png -------------------------------------------------------------------------------- /shared/image/icon-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/icon-settings.png -------------------------------------------------------------------------------- /shared/image/icon-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/icon-terminal.png -------------------------------------------------------------------------------- /shared/image/loader-btn-info.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-info.gif -------------------------------------------------------------------------------- /shared/image/loader-btn-link.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-link.gif -------------------------------------------------------------------------------- /shared/image/tool-icon-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-code.png -------------------------------------------------------------------------------- /shared/image/tool-icon-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-map.png -------------------------------------------------------------------------------- /shared/image/tool-icon-sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-sql.png -------------------------------------------------------------------------------- /shared/image/tool-icon-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-test.png -------------------------------------------------------------------------------- /shared/image/tractor-500x320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tractor-500x320.png -------------------------------------------------------------------------------- /shared/vendor/js/lang-apollo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-apollo.js -------------------------------------------------------------------------------- /shared/vendor/js/lang-scala.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/lang-scala.js -------------------------------------------------------------------------------- /.ssh/config: -------------------------------------------------------------------------------- 1 | Host github.com 2 | Hostname github.com 3 | User git 4 | IdentityFile /etc/custard/tools_rsa 5 | -------------------------------------------------------------------------------- /shared/image/chooser-icon-24px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/chooser-icon-24px.png -------------------------------------------------------------------------------- /shared/image/header-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/header-highlight.png -------------------------------------------------------------------------------- /shared/image/icon-input-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/icon-input-search.png -------------------------------------------------------------------------------- /shared/image/loader-btn-danger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-danger.gif -------------------------------------------------------------------------------- /shared/image/tile-options-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tile-options-grey.png -------------------------------------------------------------------------------- /shared/image/tool-icon-classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-classic.png -------------------------------------------------------------------------------- /shared/image/tool-icon-events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-events.png -------------------------------------------------------------------------------- /shared/image/tool-icon-lastfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-lastfm.png -------------------------------------------------------------------------------- /shared/image/tool-icon-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-network.png -------------------------------------------------------------------------------- /shared/image/tool-icon-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-report.png -------------------------------------------------------------------------------- /shared/image/tool-icon-twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-twitter.png -------------------------------------------------------------------------------- /shared/vendor/js/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/js/ZeroClipboard.swf -------------------------------------------------------------------------------- /client/code/namespace.coffee: -------------------------------------------------------------------------------- 1 | window.Cu = 2 | Model: {} 3 | Collection: {} 4 | View: {} 5 | Router: {} 6 | Util: {} 7 | -------------------------------------------------------------------------------- /docker/populate-node-modules.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | cp /data/custard/package.json . 4 | npm install --unsafe-perm 5 | 6 | -------------------------------------------------------------------------------- /shared/image/dataset-tools-toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/dataset-tools-toggle.png -------------------------------------------------------------------------------- /shared/image/loader-btn-default.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-default.gif -------------------------------------------------------------------------------- /shared/image/loader-btn-inverse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-inverse.gif -------------------------------------------------------------------------------- /shared/image/loader-btn-primary.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-primary.gif -------------------------------------------------------------------------------- /shared/image/loader-btn-success.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-success.gif -------------------------------------------------------------------------------- /shared/image/loader-btn-warning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-btn-warning.gif -------------------------------------------------------------------------------- /shared/image/loader-input-search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/loader-input-search.gif -------------------------------------------------------------------------------- /shared/image/quickcode-rough-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/quickcode-rough-logo.png -------------------------------------------------------------------------------- /shared/image/tile-options-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tile-options-white.png -------------------------------------------------------------------------------- /shared/image/tool-icon-data-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-data-table.png -------------------------------------------------------------------------------- /shared/image/tool-icon-summarise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-summarise.png -------------------------------------------------------------------------------- /shared/image/toolbar-scroll-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/toolbar-scroll-left.png -------------------------------------------------------------------------------- /shared/image/toolbar-scroll-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/toolbar-scroll-right.png -------------------------------------------------------------------------------- /shared/image/tractor-metro-tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tractor-metro-tile.png -------------------------------------------------------------------------------- /docker/custard-data-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM custard 2 | 3 | ADD package.json /data/package.json 4 | 5 | VOLUME /data/node_modules 6 | -------------------------------------------------------------------------------- /shared/image/toolbar-triangle-mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/toolbar-triangle-mask.png -------------------------------------------------------------------------------- /cron/find-unsubscribed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source /etc/custard/production-activate 4 | /opt/custard/bin/find-unsubscribed.coffee 5 | -------------------------------------------------------------------------------- /shared/image/screenshots/tool-chooser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/tool-chooser.png -------------------------------------------------------------------------------- /shared/image/screenshots/twitter-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/twitter-auth.png -------------------------------------------------------------------------------- /shared/image/tool-icon-linkedin-groups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-linkedin-groups.png -------------------------------------------------------------------------------- /shared/image/tool-icon-linkedin-people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-linkedin-people.png -------------------------------------------------------------------------------- /shared/image/tool-icon-notifications.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-notifications.png -------------------------------------------------------------------------------- /client/template/dataset-row-deleted.eco: -------------------------------------------------------------------------------- 1 | 2 | Dataset deleted 3 | 4 | -------------------------------------------------------------------------------- /shared/image/screenshots/twitter-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/twitter-toolbar.png -------------------------------------------------------------------------------- /shared/vendor/image/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/image/glyphicons-halflings.png -------------------------------------------------------------------------------- /shared/image/screenshots/create-new-dataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/create-new-dataset.png -------------------------------------------------------------------------------- /shared/image/screenshots/importer-chooser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/importer-chooser.png -------------------------------------------------------------------------------- /shared/image/screenshots/twitter-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/twitter-download.png -------------------------------------------------------------------------------- /shared/image/screenshots/twitter-schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/twitter-schedule.png -------------------------------------------------------------------------------- /shared/image/screenshots/twitter-table-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/twitter-table-view.png -------------------------------------------------------------------------------- /shared/image/tool-icon-linkedin-companies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-linkedin-companies.png -------------------------------------------------------------------------------- /shared/image/tool-icon-spreadsheet-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/tool-icon-spreadsheet-upload.png -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --ui bdd 3 | --compilers coffee:coffee-script/register 4 | --reporter spec 5 | --timeout 40000 6 | --recursive -------------------------------------------------------------------------------- /shared/image/screenshots/twitter-search-term.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/twitter-search-term.png -------------------------------------------------------------------------------- /shared/image/screenshots/code-in-browser-rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/code-in-browser-rename.png -------------------------------------------------------------------------------- /shared/image/screenshots/code-in-browser-toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/code-in-browser-toolbar.png -------------------------------------------------------------------------------- /shared/image/screenshots/code-your-own-tool-ssh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/code-your-own-tool-ssh.png -------------------------------------------------------------------------------- /shared/vendor/image/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/vendor/image/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /shared/image/screenshots/code-in-browser-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/code-in-browser-language.png -------------------------------------------------------------------------------- /shared/image/screenshots/code-your-own-tool-rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/code-your-own-tool-rename.png -------------------------------------------------------------------------------- /shared/image/screenshots/code-your-own-tool-finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/code-your-own-tool-finished.png -------------------------------------------------------------------------------- /shared/image/screenshots/default-dataset-index-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantabular/custard/HEAD/shared/image/screenshots/default-dataset-index-html.png -------------------------------------------------------------------------------- /shared/style/ie.css: -------------------------------------------------------------------------------- 1 | /* IE8-specific CSS styles for ScraperWiki. 2 | We do not support IE6-7. */ 3 | 4 | .home .hero-unit img { 5 | width: 220px; 6 | height: 141px; 7 | } -------------------------------------------------------------------------------- /client/code/view/people-pack.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.PeoplePack extends Backbone.View 2 | className: "toolpack" 3 | 4 | render: -> 5 | @el.innerHTML = JST['people-pack']() 6 | @ -------------------------------------------------------------------------------- /client/template/fourohfour.eco: -------------------------------------------------------------------------------- 1 |

Sorry! We couldn’t find what you’re looking for.

2 |

Have a micropig.

3 | Micro pig 4 | -------------------------------------------------------------------------------- /client/code/view/fourohfour.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.FourOhFour extends Backbone.View 2 | className: "fourohfour error-page" 3 | 4 | render: -> 5 | @el.innerHTML = JST['fourohfour']() 6 | @ 7 | -------------------------------------------------------------------------------- /client/template/subscribe.eco: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Loading…

4 |
5 | -------------------------------------------------------------------------------- /pre-commit.sh: -------------------------------------------------------------------------------- 1 | # pre-commit.sh 2 | . activate 3 | git stash -q --keep-index 4 | ./run_tests.sh 5 | RESULT=$? 6 | git stash pop -q 7 | [ $RESULT -ne 0 ] && exit 1 8 | mocha test/unit 9 | exit 0 10 | -------------------------------------------------------------------------------- /client/template/subnav.eco: -------------------------------------------------------------------------------- 1 | 6 |
7 | -------------------------------------------------------------------------------- /bin/mongo-timestamp: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var id = process.argv[2] 4 | var timehex = id.substring(0,8); 5 | var secondsSinceEpoch = parseInt(timehex, 16); 6 | var dt = new Date(secondsSinceEpoch*1000); 7 | console.log(dt); 8 | -------------------------------------------------------------------------------- /client/code/view/home.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.Home extends Backbone.View 2 | className: 'home' 3 | 4 | initialize: (options) -> 5 | @options = options || {} 6 | 7 | render: -> 8 | window.location.href = "/datasets" 9 | 10 | -------------------------------------------------------------------------------- /client/template/help-upload-and-summarise.eco: -------------------------------------------------------------------------------- 1 |

1. Find a spreadsheet

2 | 3 |
4 | 5 |

2. Upload it

6 | 7 |
8 | 9 |

3. Fix errors (if there are any!)

10 | 11 |
12 | 13 |

4. Summarise it

-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /server/js/ 3 | /shared/js/ 4 | /chromedriver.log 5 | /mongo 6 | /chromedriver_* 7 | /chromedriver 8 | /selenium-*.jar 9 | /request.csv 10 | /.cache/ 11 | /.node-gyp/ 12 | /.npm/ 13 | /builtAssets/ 14 | /tmp/ 15 | -------------------------------------------------------------------------------- /test/unit/model/plan.coffee: -------------------------------------------------------------------------------- 1 | require '../setup_teardown' 2 | 3 | sinon = require 'sinon' 4 | should = require 'should' 5 | _ = require 'underscore' 6 | request = require 'request' 7 | 8 | {Plan} = require 'model/plan' 9 | {Box} = require 'model/box' 10 | 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | web: 2 | environment: 3 | - CU_DB=mongodb://db/dev 4 | - CU_SESSION_SECRET=foo 5 | - NODE_PATH=/usr/src/app/server/code 6 | build: . 7 | links: 8 | - db 9 | ports: 10 | - "3001:3001" 11 | db: 12 | image: mongo:2.6 13 | 14 | -------------------------------------------------------------------------------- /migration/update_tools.js: -------------------------------------------------------------------------------- 1 | // Run with mongo -u -p 2 | // Dataset names are tool names, so set the tool field to the name field 3 | db.datasets.find().forEach( 4 | function (elem) { 5 | elem.tool = elem.name 6 | db.datasets.save(elem); 7 | } 8 | ) 9 | -------------------------------------------------------------------------------- /test/unit/setup_teardown.coffee: -------------------------------------------------------------------------------- 1 | cleaner = require '../cleaner' 2 | 3 | before (done) -> 4 | console.log "[scraperwiki global before]" 5 | 6 | cleaner.clear_and_set_fixtures -> 7 | done() 8 | 9 | after (done) -> 10 | console.log "[scraperwiki global after]" 11 | done() 12 | -------------------------------------------------------------------------------- /client/code/view/thankyou.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.Thankyou extends Backbone.View 2 | className: 'thankyou' 3 | 4 | render: -> 5 | options = 6 | hasAccount: false 7 | if window.user?.real 8 | options.hasAccount = true 9 | @el.innerHTML = JST['thankyou'] options 10 | @ 11 | 12 | -------------------------------------------------------------------------------- /shared/vendor/js/lang-proto.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.sourceDecorator({keywords:"bytes,default,double,enum,extend,extensions,false,group,import,max,message,option,optional,package,repeated,required,returns,rpc,service,syntax,to,true",types:/^(bool|(double|s?fixed|[su]?int)(32|64)|float|string)\b/,cStyleComments:!0}),["proto"]); 2 | -------------------------------------------------------------------------------- /util/listAllUserBoxes: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | 4 | mongoose = require 'mongoose' 5 | async = require 'async' 6 | 7 | {Box} = require 'model/box' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | Box.findAllByUser process.argv[2], (err, boxen) -> 12 | for box in boxen 13 | console.log box.name 14 | process.exit() 15 | -------------------------------------------------------------------------------- /util/generateShadowLines: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | 4 | mongoose = require 'mongoose' 5 | async = require 'async' 6 | 7 | {Box} = require 'model/box' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | Box.find {server: process.argv[2]}, (err, boxen) -> 12 | for box in boxen 13 | console.log "#{box.name}:x:15607:0:99999:7:::" 14 | process.exit() 15 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scraperwiki/base:precise 2 | 3 | ENV DEBIAN_FRONTEND noninteractive 4 | 5 | RUN apt-get update 6 | RUN apt-get install -y mongodb 7 | RUN apt-get install -y python-software-properties 8 | 9 | RUN apt-add-repository -y ppa:chris-lea/node.js 10 | RUN apt-get update 11 | RUN apt-get install -y nodejs 12 | RUN apt-get install -y netcat lsof 13 | 14 | RUN mkdir /opt/custard -------------------------------------------------------------------------------- /util/generatePasswdLines: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | 4 | mongoose = require 'mongoose' 5 | async = require 'async' 6 | 7 | {Box} = require 'model/box' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | Box.find {server: process.argv[2]}, (err, boxen) -> 12 | for box in boxen 13 | console.log "#{box.name}:x:#{box.uid}:10000::/home:/bin/bash" 14 | process.exit() 15 | -------------------------------------------------------------------------------- /client/template/modal-upgrade.eco: -------------------------------------------------------------------------------- 1 | 5 | 8 | 11 | -------------------------------------------------------------------------------- /util/listUsersOnPlan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | 4 | mongoose = require 'mongoose' 5 | async = require 'async' 6 | 7 | {User} = require 'model/user' 8 | 9 | 10 | mongoose.connect process.env.CU_DB 11 | User.find accountLevel: process.argv[2], (err, users) -> 12 | async.eachLimit users, 1, (user, callback) -> 13 | console.log user.shortName 14 | callback() 15 | , -> 16 | process.exit() 17 | -------------------------------------------------------------------------------- /client/template/helpnav.eco: -------------------------------------------------------------------------------- 1 |
14 | -------------------------------------------------------------------------------- /client/template/modal-downgrade.eco: -------------------------------------------------------------------------------- 1 | 5 | 8 | 11 | -------------------------------------------------------------------------------- /util/transferBoxData.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BOX_NAME="$1" 4 | SERVER="$2" 5 | case $SERVER in 6 | (*ec2*) 7 | rsync --archive --verbose -e 'ssh' --rsync-path 'sudo rsync' ubuntu@${SERVER}:/home/$BOX_NAME/ ${CO_STORAGE_DIR}/home/$BOX_NAME 8 | ;; 9 | (*) 10 | rsync --archive --verbose -e 'ssh -oIdentitiesOnly=yes -i /tmp/something' root@${SERVER}:/home/$BOX_NAME/ ${CO_STORAGE_DIR}/home/$BOX_NAME 11 | ;; 12 | esac 13 | -------------------------------------------------------------------------------- /shared/vendor/js/lang-yaml.js: -------------------------------------------------------------------------------- 1 | var a=null; 2 | PR.registerLangHandler(PR.createSimpleLexer([["pun",/^[:>?|]+/,a,":|>?"],["dec",/^%(?:YAML|TAG)[^\n\r#]+/,a,"%"],["typ",/^&\S+/,a,"&"],["typ",/^!\S*/,a,"!"],["str",/^"(?:[^"\\]|\\.)*(?:"|$)/,a,'"'],["str",/^'(?:[^']|'')*(?:'|$)/,a,"'"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^\s+/,a," \t\r\n"]],[["dec",/^(?:---|\.\.\.)(?:[\n\r]|$)/],["pun",/^-/],["kwd",/^\w+:[\n\r ]/],["pln",/^\w+/]]),["yaml","yml"]); 3 | -------------------------------------------------------------------------------- /server/code/model/plan.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | request = require 'request' 3 | plans = require 'plans.json' 4 | 5 | class exports.Plan 6 | @getPlan: (planName) -> 7 | plan = plans[planName] 8 | if not plan? 9 | return ["Plan #{planName} not found", null] 10 | else 11 | if process.env.CU_BOX_SERVER 12 | plan = _.clone(plan) 13 | plan.boxServer = process.env.CU_BOX_SERVER 14 | return [null, plan] 15 | -------------------------------------------------------------------------------- /client/template/signupnav.eco: -------------------------------------------------------------------------------- 1 | 14 |
15 | -------------------------------------------------------------------------------- /shared/sitemap.txt: -------------------------------------------------------------------------------- 1 | https://quickcode.io/ 2 | https://app.quickcode.io/help/ 3 | https://app.quickcode.io/help/code-in-your-browser/ 4 | https://app.quickcode.io/help/corporate/ 5 | https://app.quickcode.io/help/developer/ 6 | https://app.quickcode.io/help/make-your-own-tool/ 7 | https://app.quickcode.io/help/whats-new/ 8 | https://app.quickcode.io/help/zig/ 9 | https://app.quickcode.io/login/ 10 | https://app.quickcode.io/pricing/ 11 | https://app.quickcode.io/terms/ 12 | -------------------------------------------------------------------------------- /test/cleaner.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | clear_and_set_fixtures = (done) -> 4 | fixtures = require('pow-mongodb-fixtures').connect('cu-test') 5 | fixtures.clearAllAndLoad __dirname + '/../fixtures.js', (err) -> 6 | if err 7 | console.error(err) 8 | return process.exit(99) 9 | done() 10 | 11 | exports.clear_and_set_fixtures = clear_and_set_fixtures 12 | 13 | if require.main == module 14 | clear_and_set_fixtures -> 15 | process.exit 0 16 | -------------------------------------------------------------------------------- /shared/template/termsfeed.rss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | New QuickCode Terms and Conditions 6 | https://quickcode.io/ 7 | Changes to our website terms 8 | 9 | Version 2 10 | https://app.quickcode.io/terms 11 | This is the second version. 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/template/reset-password.eco: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 | 6 |

7 |

8 | 9 |

10 |
11 |
12 | -------------------------------------------------------------------------------- /client/code/view/dataset/deleted.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.DeletedDataset extends Backbone.View 2 | className: "deleted-dataset" 3 | 4 | events: 5 | 'click #recover': 'recover' 6 | 7 | render: -> 8 | console.log @model 9 | @el.innerHTML = """ 10 |

That dataset has been deleted.

11 | Contact us for recovery 12 | """ 13 | @ 14 | 15 | recover: => 16 | window.Intercom('show') 17 | 18 | -------------------------------------------------------------------------------- /util/listBoxServersOfDir: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | 4 | mongoose = require 'mongoose' 5 | async = require 'async' 6 | 7 | {Box} = require 'model/box' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | listBoxServer = (file, cb) -> 12 | Box.dbClass.findOne {name: file}, (err, box) -> 13 | if box? 14 | console.log "#{box.name} #{box.server}" 15 | return cb null, null 16 | 17 | files = fs.readdirSync process.argv[2] 18 | 19 | async.each files, listBoxServer, -> 20 | process.exit 0 21 | -------------------------------------------------------------------------------- /server/template/base_footer.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/integration/home_nologin.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, home_url, login_url} = require './helper' 4 | 5 | describe 'Home page (not logged in)', -> 6 | 7 | before (done) -> 8 | browser.deleteAllCookies done 9 | 10 | context 'when I visit scraperwiki.com/datasets without logging in', -> 11 | 12 | before (done) -> 13 | browser.get home_url, done 14 | 15 | it 'I am redirected to the login page', -> 16 | wd40.trueURL (err, url) -> 17 | url.should.equal login_url 18 | -------------------------------------------------------------------------------- /client/template/thankyou.eco: -------------------------------------------------------------------------------- 1 |
2 | The QuickCode tractor says Hi 3 | 4 |

Thankyou for signing up to QuickCode!

5 | 6 | <% if @hasAccount: %> 7 |

Go to your datasets

8 | <% else: %> 9 |

Please check your email inbox for an activation link.

10 | <% end %> 11 | 12 |
13 | -------------------------------------------------------------------------------- /util/changeUserPlan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | 4 | mongoose = require 'mongoose' 5 | async = require 'async' 6 | 7 | {User} = require 'model/user' 8 | 9 | 10 | mongoose.connect process.env.CU_DB 11 | User.find accountLevel: "grandfather", (err, users) -> 12 | async.eachLimit users, 1, (user, callback) -> 13 | console.log user.shortName 14 | user.accountLevel = "grandfather-ec2" 15 | console.log user.accountLevel 16 | user.save (err) -> 17 | if err 18 | console.log err 19 | callback() 20 | , -> 21 | process.exit() 22 | -------------------------------------------------------------------------------- /shared/vendor/js/lang-hs.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n \r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^\n\f\r'\\]|\\[^&])'?/,null,"'"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^(?:--+[^\n\f\r]*|{-(?:[^-]|-+[^}-])*-})/],["kwd",/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^\d'A-Za-z]|$)/, 2 | null],["pln",/^(?:[A-Z][\w']*\.)*[A-Za-z][\w']*/],["pun",/^[^\d\t-\r "'A-Za-z]+/]]),["hs"]); 3 | -------------------------------------------------------------------------------- /client/template/view-tile.eco: -------------------------------------------------------------------------------- 1 |

<%= @displayName %>

2 | 12 | -------------------------------------------------------------------------------- /client/template/set-password.eco: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if @shortName: %> 4 |

5 | 6 | <%= @shortName %> 7 |

8 | <% end %> 9 |

10 | 11 | 12 |

13 |

14 | 15 |

16 |
17 |
18 | -------------------------------------------------------------------------------- /test/unit/view/error-bar.coffee: -------------------------------------------------------------------------------- 1 | require '../setup_teardown' 2 | 3 | sinon = require 'sinon' 4 | should = require 'should' 5 | 6 | helper = require '../helper' 7 | unless Cu.View.ErrorAlert? 8 | helper.evalConcatenatedFile 'client/code/view/error.coffee' 9 | 10 | describe 'View: ErrorAlert', -> 11 | context 'when we trigger an "error" event on the global event bus', -> 12 | before -> 13 | @onErrorStub = sinon.stub Cu.View.ErrorAlert.prototype, 'onError' 14 | @view = new Cu.View.ErrorAlert 15 | Backbone.trigger 'error', 'foo' 16 | 17 | it 'calls the onError function', -> 18 | @onErrorStub.calledOnce.should.be.true 19 | -------------------------------------------------------------------------------- /migration/090addMeToMe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | 5 | {User} = require 'model/user' 6 | 7 | mongoose.connect process.env.CU_DB 8 | 9 | reallyMe = (shortName, cb) -> 10 | User.findByShortName shortName, (err, user) -> 11 | console.log "user", shortName 12 | if user.canBeReally is null 13 | user.canBeReally = [shortName] 14 | else 15 | user.canBeReally.push shortName 16 | user.acceptedTerms = 1 17 | user.save (err, user) -> 18 | console.log "user #{shortName} err #{err}" 19 | cb() 20 | 21 | console.log process.argv[2..] 22 | async.eachLimit process.argv[2..], 1, reallyMe, -> process.exit() 23 | -------------------------------------------------------------------------------- /migration/100addToBeDeleted: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | 5 | {Dataset} = require 'model/dataset' 6 | 7 | mongoose.connect process.env.CU_DB 8 | 9 | 10 | addToBeleted = (dataset, callback) -> 11 | fiveMinutesInFuture = new Date(new Date().getTime() + 5 * 60000) 12 | dataset.toBeDeleted = fiveMinutesInFuture 13 | if dataset.tool is null 14 | dataset.tool = 'fakingtool' 15 | dataset.save callback 16 | 17 | Dataset.find 18 | state: 'deleted' 19 | toBeDeleted: 20 | $exists: false 21 | , (err, dataseten) -> 22 | async.eachLimit dataseten, 1, addToBeleted, (err) -> 23 | console.log err if err? 24 | process.exit() 25 | -------------------------------------------------------------------------------- /util/addUnixUser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | USERNAME="$1" 4 | UID="$2" 5 | # TODO: exit when already exists 6 | # Create the user 7 | gid=$(awk -F: '/^databox:/{print $3}' /etc/group) 8 | passwd_row="${USERNAME}:x:${UID}:${gid}::/home:/bin/bash" 9 | shadow_row="${USERNAME}:x:15607:0:99999:7:::" 10 | ( 11 | flock -w 2 9 || exit 99 12 | { cat ${CO_STORAGE_DIR}/etc/passwd ; echo "$passwd_row" ; } > ${CO_STORAGE_DIR}/etc/passwd+ 13 | mv ${CO_STORAGE_DIR}/etc/passwd+ ${CO_STORAGE_DIR}/etc/passwd 14 | { cat ${CO_STORAGE_DIR}/etc/shadow ; echo "$shadow_row" ; } > ${CO_STORAGE_DIR}/etc/shadow+ 15 | mv ${CO_STORAGE_DIR}/etc/shadow+ ${CO_STORAGE_DIR}/etc/shadow 16 | ) 9> ${CO_STORAGE_DIR}/etc/passwd.cobalt.lock 17 | -------------------------------------------------------------------------------- /client/template/dataset-views.eco: -------------------------------------------------------------------------------- 1 |

Views on this data:

2 |
3 |

View Spreadsheet

4 |

Author: QuickCode

5 |
6 |
7 |

Download CSV

8 |

Author: QuickCode

9 |
10 |
11 |

View Source

12 |

Author: QuickCode

13 |
14 | -------------------------------------------------------------------------------- /client/template/subnav-home.eco: -------------------------------------------------------------------------------- 1 | 6 | 18 | -------------------------------------------------------------------------------- /client/code/model/boxable.coffee: -------------------------------------------------------------------------------- 1 | class Cu.Boxable 2 | exec: (cmd, args) -> 3 | # Returns an ajax object, onto which you can 4 | # chain .success and .error callbacks 5 | boxurl = "#{@endpoint()}/#{@get 'box'}" 6 | settings = 7 | url: "#{boxurl}/exec" 8 | type: 'POST' 9 | dataType: 'text' 10 | data: 11 | apikey: window.user.effective.apiKey 12 | cmd: cmd 13 | if args? 14 | $.extend settings, args 15 | $.ajax settings 16 | 17 | endpoint: -> 18 | server = @get('boxServer') or @get('server') 19 | #TODO: stop poluting the global namespace 20 | "https://#{server}" 21 | 22 | 23 | @mixin: (klass) -> 24 | _.extend klass.prototype, @prototype 25 | -------------------------------------------------------------------------------- /bin/mongurl: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | out () { 4 | printf '%s\n' "$1" 5 | } 6 | die () { 7 | printf '%s\n' "$1" 1>&2 8 | exit 4 9 | } 10 | 11 | usage () { 12 | out "mongurl [-p] mongodb://..." 13 | } 14 | 15 | print=false 16 | while [ $# -gt 0 ] 17 | do 18 | case $1 in 19 | (-p|--print) shift; print=true;; 20 | (-*) usage 1>&2; exit 2;; 21 | (*) break;; 22 | esac 23 | done 24 | 25 | # Validate input 26 | out "$1" | grep '^mongodb://' > /dev/null || 27 | die "Mongo URI should start mongodb://" 28 | 29 | split () { 30 | sed 's,^mongodb://\([^:]*\):\([^@]*\)@\(.*\),--username \1 --password \2 \3,' 31 | } 32 | 33 | if $print 34 | then 35 | out "$1" | split 36 | exit 0 37 | fi 38 | 39 | mongo $(out "$1" | split) $2 40 | -------------------------------------------------------------------------------- /client/code/util.coffee: -------------------------------------------------------------------------------- 1 | handleError = (old) -> 2 | -> 3 | args = _.toArray arguments 4 | if args.length == 0 5 | options = {} 6 | args.push options 7 | else 8 | options = args[args.length - 1] || {} 9 | unless options.error? 10 | options.error = (collection, response, options) -> 11 | Backbone.trigger 'error', collection, response, options 12 | old.apply this, args 13 | 14 | patchErrors = -> 15 | Backbone.Collection.prototype.fetch = handleError Backbone.Collection.prototype.fetch 16 | Backbone.Model.prototype.save = handleError Backbone.Model.prototype.save 17 | Backbone.Model.prototype.fetch = handleError Backbone.Model.prototype.fetch 18 | 19 | window.Cu.Util.patchErrors = patchErrors 20 | -------------------------------------------------------------------------------- /test/integration/errors.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, loginAndGo} = require './helper' 4 | 5 | describe 'Errors', -> 6 | 7 | context 'when jQuery receives a 502 error from an AJAX call', -> 8 | before (done) -> 9 | loginAndGo "ehg", "testing", "/datasets", done 10 | 11 | before (done) -> 12 | browser.eval "jQuery.ajax({url: 'http://httpbin.org/status/502'});", done 13 | 14 | it 'shows the error bar', (done) -> 15 | browser.waitForVisibleByCssSelector '#error-alert', 10000, done 16 | 17 | it 'displays a connection error', (done) -> 18 | wd40.getText '#error-alert', (err, text) -> 19 | text.should.include "We couldn't connect" 20 | done err 21 | -------------------------------------------------------------------------------- /server/code/lib/sign.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | qs = require 'qs' 3 | 4 | toNestedQuerystring = (obj) -> 5 | qs.stringify(obj) 6 | 7 | exports.sign = (data) -> 8 | unless process.env.RECURLY_PRIVATE_KEY? 9 | throw {error: 'Recurly.js private key is not set.'} 10 | if 'timestamp' not in data 11 | data.timestamp = Math.round (Date.now() / 1000) 12 | if 'nonce' not in data 13 | randomBytes = crypto.randomBytes 32 14 | randomString = Buffer(randomBytes).toString 'base64' 15 | data.nonce = randomString.replace /\W+/g, '' 16 | 17 | unsigned = toNestedQuerystring data 18 | hmac = crypto.createHmac('sha1', process.env.RECURLY_PRIVATE_KEY) 19 | signed = hmac.update(unsigned).digest('hex') 20 | return [signed, unsigned].join '|' 21 | -------------------------------------------------------------------------------- /shared/vendor/js/lang-lisp.js: -------------------------------------------------------------------------------- 1 | var a=null; 2 | PR.registerLangHandler(PR.createSimpleLexer([["opn",/^\(+/,a,"("],["clo",/^\)+/,a,")"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \xa0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:block|c[ad]+r|catch|con[ds]|def(?:ine|un)|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/,a], 3 | ["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["cl","el","lisp","scm"]); 4 | -------------------------------------------------------------------------------- /migration/050addBoxServers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | 5 | {User} = require 'model/user' 6 | {Box} = require 'model/box' 7 | plans = require 'plans.json' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | addBoxServer = (box, callback) -> 12 | console.log box 13 | User.findByShortName box.users[0], (err, user) -> 14 | if err? 15 | console.log err 16 | return callback err 17 | else 18 | server = plans[user.accountLevel]?.boxServer 19 | if server? 20 | box.server = server 21 | box.save callback 22 | else 23 | console.log "NO SERVER for #{user.shortName}" 24 | return callback 'NO SERVER' 25 | 26 | Box.dbClass.find {server: null}, (err, boxen) -> 27 | async.eachSeries boxen, addBoxServer, (err) -> 28 | console.log err if err? 29 | -------------------------------------------------------------------------------- /test/unit/model/view.coffee: -------------------------------------------------------------------------------- 1 | require '../setup_teardown' 2 | 3 | sinon = require 'sinon' 4 | should = require 'should' 5 | 6 | describe 'Client model: View', -> 7 | helper = require '../helper' 8 | unless Cu.Model.Tool? 9 | helper.evalConcatenatedFile 'client/code/model/tool.coffee' 10 | unless Cu.Model.View? 11 | helper.evalConcatenatedFile 'client/code/model/view.coffee' 12 | 13 | describe 'URL', -> 14 | beforeEach -> 15 | @tool = Cu.Model.Tool.findOrCreate 16 | name: 'test-plugin' 17 | manifest: 18 | displayName: 'Test Plugin' 19 | 20 | @view = Cu.Model.View.findOrCreate 21 | user: 'test' 22 | box: 'box1' 23 | tool: 'test-plugin' 24 | 25 | it 'has a related tool', -> 26 | tool = @view.get('tool') 27 | tool.get('manifest').displayName.should.equal 'Test Plugin' 28 | -------------------------------------------------------------------------------- /shared/vendor/js/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /migration/2013-08-symlink-migration/cobalt_symlink: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | 5 | {User} = require 'model/user' 6 | {Box} = require 'model/box' 7 | plans = require 'plans.json' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | addBoxServer = (box, callback) -> 12 | console.log box 13 | User.findByShortName box.users[0], (err, user) -> 14 | if err? 15 | console.log err 16 | return callback err 17 | else 18 | server = plans[user.accountLevel]?.boxServer 19 | if server? 20 | box.server = server 21 | box.save callback 22 | else 23 | console.log "NO SERVER for #{user.shortName}" 24 | return callback 'NO SERVER' 25 | 26 | Box.dbClass.find {server: null}, (err, boxen) -> 27 | async.eachSeries boxen, addBoxServer, (err) -> 28 | console.log err if err? 29 | -------------------------------------------------------------------------------- /bin/livemongo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A utility script for querying the live custard database. 4 | # Usage: 5 | # $ livemongo [optional-js-script-to-run] 6 | # Eg: 7 | # $ livemongo # will open up a repl 8 | # $ livemongo test.js # will run test.js on the database 9 | 10 | closest () { 11 | # Searches parent directories to find the first one that contains 12 | # "$1" then prints the full path, including the appended "$1". 13 | # example use: 14 | # cd $(closest swops-secret) 15 | 16 | while ! [ -e "$1" ] 17 | do 18 | if [ "$(pwd)" = '/' ] 19 | then 20 | # Didn't find it, blank output 21 | return 1 22 | fi 23 | cd .. 24 | done 25 | printf "%s\n" "$(pwd)/$1" 26 | } 27 | 28 | possibly_quoted=$( 29 | cd $(closest charm-secrets) 30 | sed -n '/CU_DB/s/.* \(.*\)/\1/p' config/live/custard.yaml) 31 | 32 | eval url=$possibly_quoted 33 | cd $(closest custard) 34 | bin/mongurl $url $1 35 | -------------------------------------------------------------------------------- /test/integration/setup_teardown.coffee: -------------------------------------------------------------------------------- 1 | {parallel} = require 'async' 2 | cleaner = require '../cleaner' 3 | {wd40, browser} = require 'wd40' 4 | 5 | base_url = process.env.CU_TEST_URL ? 'http://localhost:3001' 6 | login_url = "#{base_url}/login" 7 | logout_url = "#{base_url}/logout" 8 | 9 | before (done) -> 10 | console.log "[scraperwiki global before]" 11 | 12 | parallel [ 13 | (cb) -> 14 | cleaner.clear_and_set_fixtures -> 15 | cb() 16 | (cb) -> 17 | wd40.init (err) -> 18 | if err 19 | cb new Error("wd40 init error: #{err} -- is your Selenium server running?") 20 | return 21 | browser.get base_url, -> 22 | cb() 23 | ], done 24 | 25 | after (done) -> 26 | console.log "[scraperwiki global after]" 27 | if process.env.BROWSER_QUIT 28 | console.log "Quitting browser" 29 | return browser.quit done 30 | done() 31 | -------------------------------------------------------------------------------- /client/template/modal-ssh.eco: -------------------------------------------------------------------------------- 1 | 5 | 11 | 15 | -------------------------------------------------------------------------------- /migration/080addDatasetsBoxJSON: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | 5 | {Box} = require 'model/box' 6 | {Dataset} = require 'model/dataset' 7 | 8 | mongoose.connect process.env.CU_DB 9 | 10 | addBoxJSON = (dataset, callback) -> 11 | console.log dataset 12 | Box.findOneByName dataset.box, (err, box) -> 13 | if not box? 14 | console.log "BOX NOT EXIST #{dataset.box}" 15 | return callback() 16 | else 17 | console.log "BOX err: #{err} box: #{box}" 18 | dataset.boxJSON = box.boxJSON 19 | async.each dataset.views, (view, cb) -> 20 | Box.findOneByName view.box, (err, bx) -> 21 | if bx then view.boxJSON = bx.boxJSON 22 | cb() 23 | , -> dataset.save callback 24 | 25 | Dataset.dbClass.find {state: { $ne: 'deleted' } }, (err, dataseten) -> 26 | async.eachLimit dataseten, 3, addBoxJSON, (err) -> 27 | console.log err if err? 28 | -------------------------------------------------------------------------------- /shared/vendor/style/prettify.css: -------------------------------------------------------------------------------- 1 | .com { color: #93a1a1; } 2 | .lit { color: #195f91; } 3 | .pun, .opn, .clo { color: #93a1a1; } 4 | .fun { color: #dc322f; } 5 | .str, .atv { color: #D14; } 6 | .kwd, .prettyprint .tag { color: #1e347b; } 7 | .typ, .atn, .dec, .var { color: teal; } 8 | .pln { color: #48484c; } 9 | 10 | .prettyprint { 11 | padding: 8px; 12 | background-color: #f7f7f9; 13 | border: 1px solid #e1e1e8; 14 | } 15 | .prettyprint.linenums { 16 | -webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 17 | -moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 18 | box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 19 | } 20 | 21 | /* Specify class=linenums on a pre to get line numbering */ 22 | ol.linenums { 23 | margin: 0 0 0 33px; /* IE indents via margin-left */ 24 | } 25 | ol.linenums li { 26 | padding-left: 12px; 27 | color: #bebec5; 28 | line-height: 20px; 29 | text-shadow: 0 1px 0 #fff; 30 | } -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -x 3 | set -e 4 | 5 | cd /opt/custard 6 | 7 | date "+%H:%M:%S.%N" 8 | cp -R /data/node_modules . 9 | date "+%H:%M:%S.%N" 10 | 11 | npm install --unsafe-perm 12 | source activate 13 | 14 | echo Starting mongo. 15 | # Disable the journal, preallocation and syncing, 16 | # since the whole database is discardable. 17 | mongod --dbpath /db --quiet --noprealloc --nojournal --syncdelay=0 & 18 | 19 | waitfor() { 20 | while ! nc -z localhost $1; 21 | do 22 | echo waiting for $2 $((i++)) 23 | sleep 0.1 24 | done 25 | } 26 | 27 | waitfor 27017 mongod 28 | 29 | # cake dev & 30 | # waitfor 3001 cake-dev 31 | 32 | # echo "Sleeping for 20" 33 | # sleep 20 34 | # echo FILES = $(lsof | wc -l) 35 | 36 | 37 | echo "Starting mocha..." 38 | set +e 39 | mocha test/unit 40 | S=$? 41 | set -e 42 | 43 | # TODO(pwaller/drj): Integration tests. 44 | 45 | # Kill mongo 46 | kill $(jobs -p) 47 | wait 48 | 49 | echo mocha exit status: $S 50 | exit $S 51 | -------------------------------------------------------------------------------- /client/template/modal-api-endpoints.eco: -------------------------------------------------------------------------------- 1 | 5 | 17 | -------------------------------------------------------------------------------- /client/template/tool-tile.eco: -------------------------------------------------------------------------------- 1 |
2 | <% if @manifest?.color: %> 3 | "> 4 | <% else: %> 5 | 6 | <% end %> 7 | <% if @manifest?.color and @manifest?.icon: %> 8 | 9 | <% else if @manifest?.color: %> 10 | <%= @manifest.displayName.charAt(0) %> 11 | <% else: %> 12 | <%= @manifest.displayName.charAt(0) %> 13 | <% end %> 14 | 15 |
16 |
17 | <% if @manifest?: %> 18 |

<%= @manifest.displayName %>

19 |

<%= @manifest.description %>

20 | <% else: %> 21 |

<%= @name %>

22 | <% end %> 23 |
24 | -------------------------------------------------------------------------------- /migration/060addDatasetServers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | 5 | {User} = require 'model/user' 6 | {Dataset} = require 'model/dataset' 7 | plans = require 'plans.json' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | addBoxServer = (dataset, callback) -> 12 | console.log dataset 13 | User.findByShortName dataset.user, (err, user) -> 14 | if err? 15 | console.log err 16 | return callback err 17 | else 18 | server = plans[user.accountLevel]?.boxServer 19 | if server? 20 | dataset.boxServer = server 21 | for view in dataset.views 22 | view.boxServer = server 23 | dataset.save callback 24 | else 25 | console.log "NO SERVER for #{user.shortName}" 26 | return callback 'NO SERVER' 27 | 28 | Dataset.dbClass.find {state: { $ne: 'deleted' }, boxServer: null }, (err, dataseten) -> 29 | async.eachLimit dataseten, 1, addBoxServer, (err) -> 30 | console.log err if err? 31 | process.exit() 32 | -------------------------------------------------------------------------------- /test/integration/dataset_limit.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, loginAndGo} = require './helper' 4 | 5 | describe '3 dataset limit for free users', -> 6 | 7 | before (done) -> 8 | loginAndGo "mrgreedy", "testing", "/datasets", done 9 | 10 | context 'when I click the "new dataset" button', -> 11 | before (done) -> 12 | wd40.click '.new-dataset', -> 13 | browser.waitForElementByCss '#chooser .tool', 4000, done 14 | 15 | context 'when I click on the newdataset tool', -> 16 | before (done) -> 17 | wd40.click '.newdataset.tool', -> 18 | browser.waitForElementByCss '.pricing', 4000, done 19 | 20 | it 'is on the pricing page', (done) -> 21 | browser.url (err, url) -> 22 | url.should.include '/pricing' 23 | done() 24 | 25 | it 'shows me an upgrade message', (done) -> 26 | wd40.getText 'body', (err, text) -> 27 | text.toLowerCase().should.include 'please upgrade' 28 | done() 29 | -------------------------------------------------------------------------------- /client/code/model/tool.coffee: -------------------------------------------------------------------------------- 1 | class Cu.Model.Tool extends Backbone.RelationalModel 2 | urlRoot: "/api/tools" 3 | Cu.Boxable.mixin this 4 | 5 | idAttribute: 'name' 6 | 7 | isBasic: -> 8 | return @get('name') in ['spreadsheet-download', 'datatables-view-tool'] 9 | 10 | isImporter: -> 11 | return @get('type') is 'importer' 12 | 13 | Cu.Model.Tool.setup() 14 | 15 | class Cu.Collection.Tools extends Backbone.Collection 16 | model: Cu.Model.Tool 17 | url: "/api/tools/" 18 | name: 'Tools' 19 | 20 | importers: -> 21 | importers = @filter (t) -> t.isImporter() 22 | new Cu.Collection.Tools importers 23 | 24 | nonimporters: -> 25 | nonimporters = @filter (t) -> not t.isImporter() 26 | new Cu.Collection.Tools nonimporters 27 | 28 | basics: -> 29 | basics = @filter (t) -> 30 | t.isBasic() 31 | new Cu.Collection.Tools basics 32 | 33 | comparator: (model) -> 34 | model.get('manifest')?.displayName 35 | 36 | findByName: (toolName) -> 37 | @find (tool) -> tool.get('name') is toolName 38 | -------------------------------------------------------------------------------- /util/listGitURLsOfDir: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | 4 | mongoose = require 'mongoose' 5 | async = require 'async' 6 | 7 | {Dataset} = require 'model/dataset' 8 | {Tool} = require 'model/tool' 9 | 10 | mongoose.connect process.env.CU_DB 11 | 12 | 13 | datasets = {} 14 | tools = {} 15 | boxes = {} 16 | 17 | Tool.dbClass.find {}, (err, tools_) -> 18 | for tool in tools_ 19 | tools[tool.name] = tool 20 | 21 | Dataset.dbClass.find {}, (err, datasets_) -> 22 | for dataset in datasets_ 23 | datasets[dataset.box] = dataset 24 | 25 | for view in dataset.views 26 | datasets[view.box] = view 27 | 28 | for boxname, box of datasets 29 | if box.tool? and tools[box.tool]? 30 | tool = tools[box.tool] 31 | boxes[boxname] = [tool.gitUrl, box.tool] 32 | 33 | files = fs.readdirSync process.argv[2] 34 | 35 | for dir in files 36 | 37 | if boxes[dir]? 38 | box = boxes[dir] 39 | console.log "#{dir} #{box[0]} #{box[1]}" 40 | 41 | process.exit 0 42 | -------------------------------------------------------------------------------- /server/code/model/token.coffee: -------------------------------------------------------------------------------- 1 | mongoose = require 'mongoose' 2 | _ = require 'underscore' 3 | 4 | ModelBase = require 'model/base' 5 | 6 | tokenSchema = new mongoose.Schema 7 | token: {type: String, unique: true} 8 | shortName: String 9 | created: {type: Date, default: Date.now} 10 | 11 | zDbToken = mongoose.model 'Token', tokenSchema 12 | 13 | class exports.Token extends ModelBase 14 | @dbClass: zDbToken 15 | 16 | @find: (token, callback) -> 17 | @dbClass.findOne {token: token}, (err, token) -> 18 | if err? 19 | callback err, null 20 | else if not token? 21 | callback 'Not found', null 22 | else 23 | newToken = new Token 24 | _.extend newToken, token.toObject() 25 | callback null, newToken 26 | 27 | 28 | @findByShortName: (shortName, callback) -> 29 | @dbClass.findOne {shortName: shortName}, (err, token) -> 30 | if err? 31 | callback err, null 32 | else if not token? 33 | callback 'Not found', null 34 | else 35 | newToken = new Token 36 | _.extend newToken, token.toObject() 37 | callback null, newToken 38 | -------------------------------------------------------------------------------- /client/template/toolbar-tile.eco: -------------------------------------------------------------------------------- 1 | id="<%= @id %>"<% end %><% if @href: %> href="<%= @href %>"<% end %> data-toolname="<%= @toolName %>"> 2 |
3 | <% if @manifest?.color: %> 4 | "> 5 | <% else: %> 6 | 7 | <% end %> 8 | <% if @manifest?.color and @manifest?.icon: %> 9 | 10 | <% else if @manifest?.color: %> 11 | <%= @manifest.displayName.charAt(0) %> 12 | <% else: %> 13 | <%= @manifest.displayName.charAt(0) %> 14 | <% end %> 15 | 16 |
17 |
18 |

<%- @manifest.displayName.twoLineWrap() %>

19 |
20 |
21 | Options 22 |
23 |
-------------------------------------------------------------------------------- /client/code/view/tool/list-header.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.ToolListHeader extends Backbone.View 2 | className: 'container header' 3 | 4 | events: 5 | 'click': (e) -> 6 | e.stopPropagation() 7 | 'keyup .search-query': 'keyupPageSearch' 8 | 9 | initialize: (options) -> 10 | @options = options || {}; 11 | 12 | render: -> 13 | if @options.type == 'importers' 14 | @$el.append('

Create a new dataset…

') 15 | else 16 | @$el.append('

What would you like to do?

') 17 | @$el.append("""
18 |
19 | 20 |
21 |
""") 22 | @ 23 | 24 | keyupPageSearch: (e) -> 25 | $input = $(e.target) 26 | if e.keyCode is 27 27 | $('#chooser .tool').show() 28 | $input.val('').blur() 29 | else 30 | t = $input.val() 31 | if t != '' 32 | $('#chooser .tool').each -> 33 | if $(this).text().toUpperCase().indexOf(t.toUpperCase()) >= 0 34 | $(this).show() 35 | else 36 | $(this).hide() 37 | else if t == '' 38 | $('#chooser .tool').show() -------------------------------------------------------------------------------- /migration/040addRecurlyAccounts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | sqlite3 = require 'sqlite3' 5 | 6 | {User} = require 'model/user' 7 | 8 | mongoose.connect process.env.CU_DB 9 | db = new sqlite3.Database "../../intranet/premium-account-migration/recurly_accounts.sqlite" 10 | 11 | getRecurlyAccounts = (callback) -> 12 | db.all 'SELECT account_code FROM Sheet1', callback 13 | 14 | setRecurlyAccount = (recurlyAccount, callback) -> 15 | match = recurlyAccount.account_code.match /[0-9]+-(\w+)/ 16 | shortName = match[1] if match? 17 | return callback() unless shortName? 18 | console.log 'shortname', shortName 19 | User.findByShortName shortName, (err, user) -> 20 | if user? 21 | user.recurlyAccount = recurlyAccount.account_code 22 | user.save (err) -> 23 | unless err? 24 | console.log "added #{recurlyAccount.account_code} to #{shortName}" 25 | callback err 26 | else 27 | callback() 28 | 29 | getRecurlyAccounts (err, recurlyAccounts) -> 30 | if err? 31 | console.warn err 32 | process exit 1 33 | async.eachSeries recurlyAccounts, setRecurlyAccount, (err) -> 34 | console.log "j'ai fini", err 35 | -------------------------------------------------------------------------------- /client/template/dataset-tile.eco: -------------------------------------------------------------------------------- 1 |

<%= @dataset.displayName %>

2 | <% if @dataset.creatorDisplayName: %> 3 |

by <%= @dataset.creatorDisplayName %>

4 | <% end %> 5 | <% if @dataset.status?.type is 'error': %> 6 | <% if @dataset.status?.message: %> 7 |

<%= @dataset.status.message %>

8 | <% else: %> 9 |

Error <%= @statusUpdatedHuman %>

10 | <% end %> 11 | <% else if @dataset.status?.type is 'ok': %> 12 | <% if @dataset.status?.message: %> 13 |

<%= @dataset.status.message %>

14 | <% else: %> 15 |

Refreshed <%= @statusUpdatedHuman %>

16 | <% end %> 17 | <% else: %> 18 |

Unknown status

19 | <% end %> 20 | Hide 21 | -------------------------------------------------------------------------------- /migration/checkServerConsistency: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | 5 | {Box} = require 'model/box' 6 | {User} = require 'model/user' 7 | {Dataset} = require 'model/dataset' 8 | plans = require 'plans.json' 9 | 10 | mongoose.connect process.env.CU_DB 11 | 12 | Box.dbClass.find {}, (err, boxen) -> 13 | console.warn "got boxes" 14 | boxByName = {} 15 | for box in boxen 16 | boxByName[box.name] = box 17 | process.stderr.write '\rbox ' + box.name + ' ' 18 | Dataset.dbClass.find {state: { $ne: 'deleted' }}, (err, dataseten) -> 19 | console.warn "\ngot datasets" 20 | for dataset in dataseten 21 | box = boxByName[dataset.box] 22 | process.stderr.write '\rdataset ' + dataset.box + ' ' 23 | if not box? 24 | console.log "\nNo box for dataset #{dataset.box}" 25 | continue 26 | # A box I deliberately made inconsistent, in order 27 | # to test this script. 28 | if dataset.box == "a5pbt3i" 29 | console.log "\n" + dataset.boxServer, box.server 30 | if dataset.boxServer != box.server 31 | console.log "\n" + dataset.box, dataset.boxServer, box.server 32 | process.exit() 33 | -------------------------------------------------------------------------------- /client/code/view/terms.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.Terms extends Backbone.View 2 | className: "terms" 3 | 4 | render: -> 5 | @el.innerHTML = JST['terms']() 6 | @ 7 | 8 | class Cu.View.TermsEnterpriseAgreement extends Backbone.View 9 | className: "terms" 10 | 11 | render: -> 12 | @el.innerHTML = JST['terms-enterprise-agreement']() 13 | @ 14 | 15 | class Cu.View.TermsAlert extends Backbone.View 16 | className: "alert alert-warning permanent" 17 | 18 | events: 19 | "click #acceptTerms": "acceptTerms" 20 | 21 | render: -> 22 | @el.innerHTML = '''

Our Terms & Conditions have changed. Read them here then accept the changes. I accept

23 | ''' 24 | @ 25 | 26 | acceptTerms: -> 27 | user = new Cu.Model.User window.user.real 28 | user.save 'acceptedTerms', window.latestTerms, 29 | type: 'put' # force backbone to issue a PUT, even though it thinks this is a new model 30 | success: (model, response, options) => 31 | window.user.real.acceptedTerms = window.latestTerms 32 | @$el.slideUp 250, => 33 | @$el.remove() 34 | -------------------------------------------------------------------------------- /migration/010camelCase: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | # Created 2012-12-17 3 | # convert properties to camelCase 4 | 5 | async = require 'async' 6 | mongoose = require 'mongoose' 7 | request = require 'request' 8 | 9 | # This is the a fake User schema 10 | Schema = mongoose.Schema 11 | userSchema = new Schema 12 | apikey: {type: String} 13 | email: [String] 14 | displayname: String 15 | displayName: String 16 | password: String 17 | isstaff: Boolean 18 | shortname: String 19 | shortName: String 20 | 21 | User = mongoose.model 'User', userSchema 22 | 23 | main = -> 24 | if not process.argv[2]? 25 | console.log "Please specify a Mongo DB connection thingy" 26 | process.exit 4 27 | mongo = process.argv[2] 28 | process.stdout.write "Connecting to #{mongo}..." 29 | mongoose.connect mongo 30 | process.stdout.write "\rConnected \n" 31 | each = (user, cb) -> 32 | if user.shortname? 33 | user.shortName = user.shortname 34 | user.shortname = undefined 35 | if user.displayname? 36 | user.displayName = user.displayname 37 | user.displayname = undefined 38 | console.log user 39 | user.save -> 40 | cb null, user 41 | User.find {}, null, {}, (err, users) -> 42 | async.map users, each, process.exit 43 | 44 | main() 45 | -------------------------------------------------------------------------------- /migration/020grandfatherPlan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | # Created 2013-03-18 3 | # Set every user to the "unlimited" (which isn't really!) plan 4 | 5 | async = require 'async' 6 | mongoose = require 'mongoose' 7 | request = require 'request' 8 | 9 | # This is the a fake User schema 10 | Schema = mongoose.Schema 11 | userSchema = new Schema 12 | shortName: {type: String, unique: true} 13 | email: [String] 14 | displayName: String 15 | password: String # encrypted, see setPassword method 16 | apikey: {type: String, unique: true} 17 | isStaff: Boolean 18 | accountLevel: String 19 | trialStarted: {type: Date, default: Date.now} 20 | created: {type: Date, default: Date.now} 21 | logoUrl: String 22 | sshKeys: [String] 23 | 24 | User = mongoose.model 'User', userSchema 25 | 26 | main = -> 27 | if not process.argv[2]? 28 | console.log "Please specify a Mongo DB connection thingy" 29 | process.exit 4 30 | mongo = process.argv[2] 31 | process.stdout.write "Connecting to #{mongo}..." 32 | mongoose.connect mongo 33 | process.stdout.write "\rConnected \n" 34 | each = (user, cb) -> 35 | user.accountLevel = "grandfather" 36 | console.log user 37 | user.save -> 38 | cb null, user 39 | User.find {}, null, {}, (err, users) -> 40 | async.map users, each, process.exit 41 | 42 | main() 43 | -------------------------------------------------------------------------------- /client/code/model/view.coffee: -------------------------------------------------------------------------------- 1 | class Cu.Model.View extends Backbone.RelationalModel 2 | Cu.Boxable.mixin this 3 | 4 | idAttribute: 'box' 5 | relations: [ 6 | { 7 | type: Backbone.HasOne 8 | key: 'tool' 9 | relatedModel: Cu.Model.Tool 10 | includeInJSON: 'name' 11 | } 12 | ] 13 | 14 | url: -> 15 | datasetId = @get('plugsInTo').get('box') 16 | if @isNew() 17 | "/api/#{window.user.effective.shortName}/datasets/#{datasetId}/views" 18 | else 19 | "/api/#{window.user.effective.shortName}/datasets/#{datasetId}/views/#{@get 'box'}" 20 | 21 | isVisible: -> 22 | @get('state') isnt 'deleted' 23 | 24 | Cu.Model.View.setup() 25 | 26 | class Cu.Collection.ViewList extends Backbone.Collection 27 | model: Cu.Model.View 28 | name: 'ViewList' 29 | url: -> "/api/#{window.user.effective.shortName}/views" 30 | 31 | findById: (id) -> 32 | views = @find (t) -> t.id is id 33 | 34 | # returns first match 35 | findByToolName: (name, callback) -> 36 | tool = app.tools().findByName name 37 | callback @find (view) -> 38 | view.get('tool') is tool and view.get('state') isnt 'deleted' 39 | 40 | visible: -> 41 | visibles = @filter (t) -> t.isVisible() 42 | new Cu.Collection.ViewList visibles 43 | 44 | comparator: (model) -> 45 | model.get 'displayName' 46 | -------------------------------------------------------------------------------- /client/template/help-home.eco: -------------------------------------------------------------------------------- 1 |

Quick start guides

2 | 3 | 10 | 11 |

General help

12 | 13 | 28 | 29 |

Developers

30 | 31 | 46 | -------------------------------------------------------------------------------- /server/template/not_found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hmmm… Something’s not right. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 37 | 38 | 39 | 40 |

Hmmm… Not found.

41 |

QuickCode can’t find that page.

42 |

43 | Go to the homepage 44 |

45 | 46 | 47 | -------------------------------------------------------------------------------- /client/template/subnav-toolbar.eco: -------------------------------------------------------------------------------- 1 | 2 | 7 |
style="background-color: <%= @color %>"<% end %>> 8 | 9 |
10 |

<%= @displayName %>

11 | <% if @creatorDisplayName: %> 12 | by <%= @creatorDisplayName %> 13 | <% end %> 14 |
15 | 16 | 17 | 18 |
19 | Options 20 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /shared/vendor/js/lang-n.js: -------------------------------------------------------------------------------- 1 | var a=null; 2 | PR.registerLangHandler(PR.createSimpleLexer([["str",/^(?:'(?:[^\n\r'\\]|\\.)*'|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,a,'"'],["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,a,"#"],["pln",/^\s+/,a," \r\n\t\xa0"]],[["str",/^@"(?:[^"]|"")*(?:"|$)/,a],["str",/^<#[^#>]*(?:#>|$)/,a],["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,a],["com",/^\/\/[^\n\r]*/,a],["com",/^\/\*[\S\s]*?(?:\*\/|$)/, 3 | a],["kwd",/^(?:abstract|and|as|base|catch|class|def|delegate|enum|event|extern|false|finally|fun|implements|interface|internal|is|macro|match|matches|module|mutable|namespace|new|null|out|override|params|partial|private|protected|public|ref|sealed|static|struct|syntax|this|throw|true|try|type|typeof|using|variant|virtual|volatile|when|where|with|assert|assert2|async|break|checked|continue|do|else|ensures|for|foreach|if|late|lock|new|nolate|otherwise|regexp|repeat|requires|return|surroundwith|unchecked|unless|using|while|yield)\b/, 4 | a],["typ",/^(?:array|bool|byte|char|decimal|double|float|int|list|long|object|sbyte|short|string|ulong|uint|ufloat|ulong|ushort|void)\b/,a],["lit",/^@[$_a-z][\w$@]*/i,a],["typ",/^@[A-Z]+[a-z][\w$@]*/,a],["pln",/^'?[$_a-z][\w$@]*/i,a],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,a,"0123456789"],["pun",/^.[^\s\w"-$'./@`]*/,a]]),["n","nemerle"]); 5 | -------------------------------------------------------------------------------- /client/code/view/dataset/tile.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.DatasetTile extends Backbone.View 2 | className: 'dataset tile swcol' 3 | tagName: 'a' 4 | attributes: -> 5 | 'data-box': @model.get 'box' 6 | 7 | events: 8 | 'click .hide': 'hideDataset' 9 | 'click .unhide': 'unhideDataset' 10 | 11 | initialize: -> 12 | @model.on 'change', @render, this 13 | @model.on 'destroy', @destroy, this 14 | 15 | render: -> 16 | if @model.get('state') is 'deleted' 17 | @$el.css 'background-color', '' 18 | @$el.removeAttr 'href' 19 | @$el.addClass 'deleted' 20 | @$el.html JST['dataset-tile-deleted'] 21 | else 22 | toolManifest = @model.get('tool')?.get('manifest') 23 | if toolManifest?.color 24 | @$el.css 'background-color', toolManifest.color 25 | @$el.attr 'href', "/dataset/#{@model.get 'box'}" 26 | @$el.removeClass 'deleted' 27 | @$el.html JST['dataset-tile'] 28 | dataset: @model.toJSON() 29 | statusUpdatedHuman: @model.statusUpdatedHuman() 30 | @ 31 | 32 | destroy: => 33 | @remove() 34 | 35 | hideDataset: (e) -> 36 | e.preventDefault() 37 | e.stopPropagation() 38 | 39 | @model.destroy() 40 | @timeout = setTimeout(@destroy, 5 * 60000) 41 | 42 | unhideDataset: (e) -> 43 | e.preventDefault() 44 | e.stopPropagation() 45 | 46 | clearTimeout(@timeout) 47 | @model.recover() 48 | -------------------------------------------------------------------------------- /shared/code/views.coffee: -------------------------------------------------------------------------------- 1 | # Backbone seems to reverse route order 2 | 3 | ScraperwikiViews = [ 4 | {route: '.*', name: 'fourOhFour'}, 5 | {route: '$', name: 'homeAnonymous'}, 6 | {route: 'datasets?/?$', name: 'homeLoggedIn'}, 7 | {route: 'dashboard/?$', name: 'dashboard'}, 8 | {route: '(?:docs|help)/?', name: 'help'}, 9 | {route: '(?:docs|help)/([^/]+)/?', name: 'help'}, 10 | {route: 'pricing/?', name: 'pricing'}, 11 | {route: 'pricing/([^/]+)/?', name: 'pricing'}, 12 | {route: 'chooser/?', name: 'toolChooser'}, 13 | {route: 'tools/people-pack/?', name: 'peoplePack'}, 14 | {route: 'dataset/([^/]+)/?', name: 'dataset'}, 15 | {route: 'dataset/([^/]+)/settings/?', name: 'datasetSettings'}, 16 | {route: 'dataset/([^/]+)/chooser/?', name: 'datasetToolChooser'}, 17 | {route: 'dataset/([^/]+)/view/([^/]+)/?', name: 'view'}, 18 | {route: 'create-profile/?', name: 'createProfile'}, 19 | {route: 'set-password/?', name: 'resetPassword'}, 20 | {route: 'set-password/([^/]+)/?', name: 'setPassword'}, 21 | {route: 'signup/([^/]+)/?', name: 'signUp'}, 22 | {route: 'subscribe/([^/]+)/?', name: 'subscribe'}, 23 | {route: 'thankyou/?', name: 'thankyou'}, 24 | {route: 'terms/?', name: 'terms'}, 25 | {route: 'terms/enterprise-agreement/?', name: 'termsEnterpriseAgreement'}, 26 | ] 27 | 28 | if exports? 29 | exports.ScraperwikiViews = ScraperwikiViews 30 | else 31 | window.ScraperwikiViews = ScraperwikiViews -------------------------------------------------------------------------------- /test/integration/terms.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, base_url, home_url, loginAndGo} = require './helper' 4 | 5 | request = require 'request' 6 | 7 | describe 'Login after introduction of Terms & Conditions', -> 8 | 9 | context 'when I log in after a long time away', -> 10 | 11 | before (done) -> 12 | browser.deleteAllCookies done 13 | 14 | before (done) -> 15 | loginAndGo "mrlazy", "testing", "/datasets", done 16 | 17 | it 'an alert bar asks me to agree to the new terms & conditions', (done) -> 18 | wd40.getText 'body', (err, text) -> 19 | text.toLowerCase().should.include 'terms & conditions have changed' 20 | done() 21 | 22 | context 'when I click the "I accept" button', -> 23 | before (done) -> 24 | wd40.click '#acceptTerms', -> 25 | setTimeout done, 1000 26 | 27 | it 'the message goes away', (done) -> 28 | wd40.getText 'body', (err, text) -> 29 | text.toLowerCase().should.not.include 'terms & conditions have changed' 30 | done() 31 | 32 | context 'and when I revist the homepage', -> 33 | before (done) -> 34 | browser.get home_url, done 35 | 36 | it 'the message is not show again', (done) -> 37 | wd40.getText 'body', (err, text) -> 38 | text.toLowerCase().should.not.include 'terms & conditions have changed' 39 | done() 40 | -------------------------------------------------------------------------------- /bin/journalistDatasets.js: -------------------------------------------------------------------------------- 1 | // Give it an object, it'll return a list of lists 2 | // ordered by the original object's keys 3 | var sortObject = function(object){ 4 | var sortable = [] 5 | for(key in object){ 6 | sortable.push([ key, object[key] ]) 7 | } 8 | return sortable.sort(function(a, b){ 9 | return a[1] - b[1] 10 | }) 11 | } 12 | 13 | var pad = function(thing, length){ 14 | return (String(thing) + Array(length+1).join(' ')).slice(0, length) 15 | } 16 | 17 | var frequency = [] 18 | 19 | print('How many datasets do "journalist" users have?') 20 | 21 | var users = db.users.find({accountLevel: "journalist"}).sort({shortName: 1}) 22 | 23 | users.forEach(function(user){ 24 | var datasets = db.datasets.find({ 25 | user: user.shortName, 26 | state: { 27 | $ne: 'deleted' 28 | } 29 | }) 30 | var n = datasets.length() 31 | if(n in frequency){ 32 | frequency[n] = frequency[n] + 1 33 | } else { 34 | frequency[n] = 1 35 | } 36 | }) 37 | 38 | print('+--------------+-------+------------+') 39 | print('| Num datasets | Users | % of users |') 40 | print('+--------------+-------+------------+') 41 | 42 | var frequenciesSorted = sortObject(frequency) 43 | 44 | for(key in frequenciesSorted){ 45 | var f = frequency[key] || 0 46 | var pc = f / users.length() 47 | print('| ' + pad(key, 12) + ' | ' + pad(f, 5) + ' | ' + pad(pc, 10) + ' |') 48 | } 49 | 50 | print('+--------------+-------+------------+') 51 | 52 | 53 | -------------------------------------------------------------------------------- /migration/070addBoxJSON: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | mongoose = require 'mongoose' 3 | async = require 'async' 4 | request = require 'request' 5 | 6 | {User} = require 'model/user' 7 | {Box} = require 'model/box' 8 | 9 | mongoose.connect process.env.CU_DB 10 | 11 | _exec = (arg, callback) -> 12 | request.post 13 | uri: "#{Box.endpoint arg.boxServer, arg.boxName}/exec" 14 | form: 15 | apikey: arg.user.apikey 16 | cmd: arg.cmd 17 | , callback 18 | 19 | addBoxJSON = (box, callback) -> 20 | if box.boxJSON?.publish_token 21 | return callback null, null 22 | console.log "#{box.server}/#{box.name}" 23 | User.findByShortName box.users[0], (err, user) -> 24 | console.log "#{box.server}/#{box.name} user #{user.shortName} #{user.apikey}" 25 | _exec 26 | cmd: "cat box.json || cat scraperwiki.json" 27 | user: user 28 | boxName: box.name 29 | boxServer: box.server 30 | , (err, res, body) -> 31 | console.log "EXEC err", err, "body", body 32 | try 33 | obj = JSON.parse body 34 | catch error 35 | console.log "ERROR PARSING", body 36 | obj = null 37 | if obj?.error 38 | console.log obj.error 39 | return callback null, null 40 | else 41 | box.boxJSON = obj 42 | box.save callback 43 | 44 | Box.dbClass.find {"boxJSON.status": {$ne: null}}, (err, boxen) -> 45 | async.eachLimit boxen, 5, addBoxJSON, (err) -> 46 | console.log err if err? 47 | process.exit() 48 | -------------------------------------------------------------------------------- /bin/custard-munin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # A Munin plugin for custard. 3 | # See http://munin.readthedocs.org/en/latest/plugin/writing.html 4 | # 5 | # In addition to the usual munin plugin protocol (call with no 6 | # arguments and call with a single "config" argument), if called 7 | # with a single "install" argument, will attempt to install itself 8 | # into /etc/munin/plugins and /etc/munin/plugin-config.d 9 | 10 | # Look for request.csv either in . or in /opt/custard 11 | File=request.csv 12 | test -r "$File" || cd /opt/custard 13 | 14 | usage () { 15 | echo 'collect-munin [config|install]' 16 | } 17 | 18 | data () { 19 | # Collect the data and truncate the file. 20 | # We don't do this atomically, so we risk losing some data. 21 | # Alternatives are too horrible to contemplate. 22 | echo response_time.value $( 23 | awk -F, '{ total += $5 }; END { print total/NR }' "$File" 24 | ) 25 | > "$File" 26 | } 27 | 28 | metadata () { 29 | echo graph_title Response time 30 | echo response_time.label Data from $(env pwd) 31 | } 32 | 33 | install () { 34 | set -e 35 | cp bin/custard-munin /etc/munin/plugins/ 36 | printf '[custard-munin] 37 | user custard 38 | ' > /etc/munin/plugin-conf.d/custard-munin 39 | } 40 | 41 | if [ $# = 0 ] 42 | then 43 | data 44 | exit 45 | fi 46 | 47 | if [ $# = 1 ] && [ "$1" = config ] 48 | then 49 | metadata 50 | exit 51 | fi 52 | 53 | if [ $# = 1 ] && [ "$1" = install ] 54 | then 55 | install 56 | exit 57 | fi 58 | 59 | usage >&2 60 | exit 99 61 | -------------------------------------------------------------------------------- /client/code/view/profile/set-password.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.SetPassword extends Backbone.View 2 | className: "set-password" 3 | 4 | initialize: (options) -> 5 | @options = options || {} 6 | 7 | events: 8 | 'click .btn-primary': 'setPassword' 9 | 10 | render: -> 11 | @el.innerHTML = JST['set-password'] @options 12 | @ 13 | 14 | setPassword: (e) -> 15 | e.preventDefault() 16 | password = $('#password').val() 17 | @$el.find('.alert').remove() 18 | @$el.find('.control-group').removeClass('error') 19 | token = location.pathname.split('/') 20 | token = token[token.length-1] 21 | $button = $(e.target) 22 | if password!='' 23 | $button.attr('disabled', true).addClass('loading').html('Setting Password…') 24 | $.ajax 25 | url: "#{location.protocol}//#{location.host}/api/token/#{token}" 26 | data: 27 | password: password 28 | type: 'POST' 29 | dataType: 'json' 30 | success: (profile) => 31 | window.location = '/datasets' 32 | error: (jqxhr, textStatus, errorThrown) => 33 | @$el.children('form').prepend """
Oh no! Something went wrong. Are you sure you clicked the right link?
""" 34 | $button.attr('disabled', false).removeClass('loading').html(' Try Again') 35 | else 36 | @$el.find('.control-group').addClass('error').children('label').text('You must supply a password:').next().focus() 37 | -------------------------------------------------------------------------------- /shared/vendor/js/lang-clj.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011 Google Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | var a=null; 17 | PR.registerLangHandler(PR.createSimpleLexer([["opn",/^[([{]+/,a,"([{"],["clo",/^[)\]}]+/,a,")]}"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \xa0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:def|if|do|let|quote|var|fn|loop|recur|throw|try|monitor-enter|monitor-exit|defmacro|defn|defn-|macroexpand|macroexpand-1|for|doseq|dosync|dotimes|and|or|when|not|assert|doto|proxy|defstruct|first|rest|cons|defprotocol|deftype|defrecord|reify|defmulti|defmethod|meta|with-meta|ns|in-ns|create-ns|import|intern|refer|alias|namespace|resolve|ref|deref|refset|new|set!|memfn|to-array|into-array|aset|gen-class|reduce|map|filter|find|nil?|empty?|hash-map|hash-set|vec|vector|seq|flatten|reverse|assoc|dissoc|list|list?|disj|get|union|difference|intersection|extend|extend-type|extend-protocol|prn)\b/,a], 18 | ["typ",/^:[\dA-Za-z-]+/]]),["clj"]); 19 | -------------------------------------------------------------------------------- /shared/template/50x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hmmm… Something’s not right. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 37 | 38 | 39 | 40 |

Hmmm… Something’s not right.

41 |

QuickCode generated a 500 server error.

42 |

43 | Reload to try again 44 | Check Twitter for announcements 45 |

46 | 47 | 48 | -------------------------------------------------------------------------------- /client/code/view/docs.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.Help extends Backbone.View 2 | className: "help" 3 | 4 | initialize: (options) -> 5 | @options = options || {} 6 | 7 | events: 8 | 'click nav a': 'navClick' 9 | 'click a[href^="#"]': 'navClick' 10 | 11 | render: -> 12 | @el.innerHTML = JST[@options.template] 13 | user: window.user.effective 14 | setTimeout @makePrettyLike, 100 15 | 16 | # redirect old URL to new page 17 | if window.location.pathname == "/help/twitter-search/" and window.location.hash == "#faq" 18 | app.navigate "/help/twitter-faq", trigger: true 19 | @ 20 | 21 | navClick: (e) -> 22 | e.preventDefault() 23 | if $(e.target.hash).length > 0 24 | app.navigate(window.location.pathname + e.target.hash) 25 | $('html, body').animate 26 | scrollTop: $(e.target.hash).offset().top - 70 27 | , 250 28 | 29 | makePrettyLike: => 30 | prettyPrint() # syntax-highlight code blocks 31 | $('nav.well').affix({offset: 110}) # fixed position for table of contents 32 | $('body').scrollspy('refresh') # highlight links in table of contents on scroll 33 | $(window).trigger('scroll') # fake a scroll event to highlight the current link 34 | $('body').on 'activate', @foldUnfold 35 | @foldUnfold() 36 | 37 | foldUnfold: -> 38 | $('.nav-list li').each -> 39 | $li = $(this) 40 | if $('.active', $li).length or $li.is('.active') 41 | $li.children('.nav-list:hidden').slideDown() 42 | else 43 | $li.find('.nav-list:visible').slideUp() 44 | -------------------------------------------------------------------------------- /client/template/modal-add-ssh.eco: -------------------------------------------------------------------------------- 1 | 5 | 29 | 33 | -------------------------------------------------------------------------------- /client/template/dataset-row.eco: -------------------------------------------------------------------------------- 1 | <% if @swatchColor: %> 2 | 3 | <% else: %> 4 | 5 | <% end %> 6 | <%= @dataset.displayName %> 7 | <% if @dataset.status?.type is 'error': %> 8 | <% if @dataset.status?.message: %> 9 | <%= @dataset.status.message %> 10 | <% else: %> 11 | Error 12 | <% end %> 13 | <% else if @dataset.status?.type is 'ok': %> 14 | <% if @dataset.status?.message: %> 15 | <%= @dataset.status.message %> 16 | <% else: %> 17 | Refreshed 18 | <% end %> 19 | <% else: %> 20 | Unknown 21 | <% end %> 22 | <% if @statusUpdatedHuman == 'Never': %> 23 | Never 24 | <% else: %> 25 | <%= @statusUpdatedHuman %> 26 | <% end %> 27 | <%- @dataset?.creatorDisplayName or 'Unknown' %> 28 | <% if @datasetCreatedHuman == 'Never': %> 29 | Never 30 | <% else: %> 31 | <%= @datasetCreatedHuman %> 32 | <% end %> 33 | 34 | -------------------------------------------------------------------------------- /client/code/view/nav.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.Nav extends Backbone.View 2 | el: '#header nav' 3 | 4 | initialize: -> 5 | if 'effective' of window.user 6 | @loggedInNav() 7 | else 8 | @loggedOutNav() 9 | @ 10 | 11 | loggedInNav: -> 12 | # Make sure we have the latest list of contexts 13 | # the current user can access. 14 | users = Cu.CollectionManager.get Cu.Collection.User 15 | users.fetch 16 | success: => 17 | real = window.user.real 18 | effective = window.user.effective 19 | allUsers = users.toJSON() 20 | 21 | staff = real?.isStaff 22 | switched = real.shortName != effective.shortName 23 | conventionalSwitch = effective.shortName in _.pluck(allUsers, 'shortName') 24 | 25 | if staff and switched and not conventionalSwitch 26 | # this is a staff member who has switched into an 27 | # account they shouldn't normally be able to access 28 | allUsers.push window.user.effective 29 | 30 | recurlyAdminUrl = undefined 31 | if window.app.cashPlan window.app.humanPlan real.accountLevel 32 | recurlyAdminUrl = "/api/#{real.shortName}/subscription/billing" 33 | isTrial = effective.accountLevel == 'free-trial' 34 | trialDaysLeft = effective.daysLeft 35 | 36 | @el.innerHTML = JST.nav 37 | realUser: real 38 | effectiveUser: effective 39 | allUsers: allUsers 40 | recurlyAdminUrl: recurlyAdminUrl 41 | isTrial: isTrial 42 | trialDaysLeft: trialDaysLeft 43 | 44 | loggedOutNav: -> 45 | @el.innerHTML = JST.nav() 46 | -------------------------------------------------------------------------------- /client/code/model/user.coffee: -------------------------------------------------------------------------------- 1 | class Cu.Model.User extends Backbone.Model 2 | idAttribute: 'shortName' 3 | url: -> "/api/user/" 4 | 5 | #TODO: hack 6 | isNew: -> true 7 | 8 | validate: (attrs) -> 9 | errors = {} 10 | if not @validDisplayName attrs 11 | errors.displayName = "This can only contain letters, numbers, spaces, dots and dashes" 12 | if not @validShortName attrs 13 | errors.shortName = "This can only contain a minimum of 3, and a maximum of 24 letters, numbers, dots and dashes" 14 | if not @validEmail attrs 15 | errors.email = "This is not a valid email address" 16 | if not @hasAcceptedTerms attrs 17 | errors.acceptedTerms = "Please accept the terms and conditions" 18 | if _.size errors 19 | return errors 20 | 21 | validDisplayName: (attrs) -> 22 | if not 'displayName' of attrs 23 | return true 24 | else 25 | return /^[^<>;\b]+$/g.test attrs.displayName 26 | 27 | validShortName: (attrs) -> 28 | if not 'shortName' of attrs 29 | return true 30 | else 31 | return /^[a-zA-Z0-9-.]{3,24}$/g.test attrs.shortName 32 | 33 | validEmail: (attrs) -> 34 | if not 'email' of attrs 35 | return true 36 | else 37 | return /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+$/gi.test attrs.email 38 | 39 | hasAcceptedTerms: (attrs) -> 40 | if not 'acceptedTerms' of attrs 41 | return true 42 | else 43 | return 0+attrs.acceptedTerms > 0 44 | 45 | class Cu.Collection.User extends Backbone.Collection 46 | url: '/api/user' 47 | name: 'User' 48 | 49 | comparator: (model) -> 50 | model.get('displayName') or model.get('shortName') 51 | -------------------------------------------------------------------------------- /test/unit/view/tile.coffee: -------------------------------------------------------------------------------- 1 | require '../setup_teardown' 2 | 3 | sinon = require('sinon') 4 | should = require('should') 5 | 6 | helper = require '../helper' 7 | unless Cu.View.DatasetTile? 8 | helper.evalConcatenatedFile 'client/code/view/dataset/tile.coffee' 9 | 10 | describe "View: DatasetTile", -> 11 | beforeEach -> 12 | Model = Backbone.Model.extend({'recover': ->}) 13 | model = new Model() 14 | sync = sinon.stub(model, 'sync') 15 | @save = sinon.stub(model, 'save') 16 | @destroy = sinon.stub(model, 'destroy') 17 | @recover = sinon.stub(model, 'recover') 18 | @view = new Cu.View.DatasetTile({model: model}) 19 | 20 | @clock = sinon.useFakeTimers() 21 | 22 | afterEach -> 23 | @clock.restore() 24 | 25 | context "When hideDataset is called", -> 26 | it "model.destroy is called", -> 27 | @view.hideDataset(jQuery.Event()) 28 | 29 | @destroy.calledOnce.should.be.true 30 | 31 | it "should setTimeout to remove tile after 5 minutes", -> 32 | @view.hideDataset(jQuery.Event()) 33 | 34 | removeSpy = sinon.spy(@view, 'remove') 35 | 36 | @clock.tick 6 * 600000 37 | 38 | removeSpy.calledOnce.should.be.true 39 | 40 | context "When unhideDataset is called", -> 41 | it "model.destroy is called", -> 42 | @view.unhideDataset(jQuery.Event()) 43 | 44 | @recover.calledOnce.should.be.true 45 | 46 | it "should clear the timeout", -> 47 | @view.hideDataset(jQuery.Event()) 48 | @view.unhideDataset(jQuery.Event()) 49 | 50 | removeSpy = sinon.spy(@view, 'remove') 51 | 52 | @clock.tick 6 * 600000 53 | 54 | removeSpy.called.should.be.false 55 | -------------------------------------------------------------------------------- /test/unit/helper.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | 3 | fakeWindow = -> 4 | {jsdom} = require 'jsdom' 5 | 6 | doc = jsdom '' 7 | global.window = doc.createWindow() 8 | global.document = global.window.document 9 | global.addEventListener = global.window.addEventListener 10 | window = global.window 11 | 12 | global.jQuery = global.$ = require('jquery').create global.window 13 | global.$.cookie = -> null 14 | global._ = window._ = require 'underscore' 15 | global.Backbone = window.Backbone = require 'backbone' 16 | global.BackboneRelational = window.BackboneRelational = require 'backbone-relational' 17 | global.Backbone.$ = global.$ 18 | 19 | # Disable BR warnings we don't care about if we're unit testing 20 | #global.Backbone.Relational.showWarnings = false 21 | 22 | exports.evalConcatenatedFile "client/code/namespace.coffee" 23 | exports.evalConcatenatedFile "client/code/model/boxable.coffee" 24 | 25 | 26 | auser = 27 | shortName: 'test' 28 | apiKey: 'fakeapikey' 29 | email: 'test@example.com' 30 | displayName: 'Tesuto Tesoto-San' 31 | 32 | global.user = 33 | effective: auser 34 | real: auser 35 | 36 | global.boxServer = process.env.CU_BOX_SERVER 37 | 38 | # Concatenate our JS and eval it 39 | exports.evalConcatenatedFile = (filepath) -> 40 | Snockets = require 'snockets' 41 | snockets = new Snockets() 42 | js = snockets.getConcatenation filepath, async: false 43 | js = js.replace /^\(function\(\) {/gm, '' 44 | js = js.replace /^}\).call\(this\);/gm, '' 45 | js = js.replace /window\./g, 'global.' # hack, so namespacing works 46 | 47 | eval.call global, js 48 | 49 | fakeWindow() 50 | -------------------------------------------------------------------------------- /client/template/sign-up.eco: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 | 20 |
21 |
22 |
23 |
24 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 39 | -------------------------------------------------------------------------------- /test/integration/tool_privacy.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, loginAndGo} = require './helper' 4 | 5 | describe 'Tool Privacy', -> 6 | 7 | context 'When ickletest (a free user) wants to make a new dataset', -> 8 | before (done) -> 9 | loginAndGo "ickletest", "toottoot", "/datasets", done 10 | 11 | before (done) -> 12 | wd40.click '.new-dataset', => 13 | browser.waitForElementByCss '#chooser .tool', 4000, => 14 | wd40.getText '#chooser', (err, text) => 15 | @chooserText = text 16 | done() 17 | 18 | it 'He can see his private tool', -> 19 | @chooserText.should.include "Ickletest's private tool" 20 | @chooserText.should.include "Mine. All mine." 21 | 22 | it 'He can see the tool for free users', -> 23 | @chooserText.should.include "Special free user tool" 24 | @chooserText.should.include "A tool only published for users on the free plan" 25 | 26 | context 'When ehg (a grandfather user) wants to make a new dataset', -> 27 | before (done) -> 28 | loginAndGo "ehg", "testing", "/datasets", done 29 | 30 | before (done) -> 31 | wd40.click '.new-dataset', => 32 | browser.waitForElementByCss '#chooser .tool', 4000, => 33 | wd40.getText '#chooser', (err, text) => 34 | @chooserText = text 35 | done() 36 | 37 | it "He cannot see Ickletest's private tool", -> 38 | @chooserText.should.not.include "Ickletest's private tool" 39 | @chooserText.should.not.include "Mine. All mine." 40 | 41 | it 'He cannot see the tool for free users', -> 42 | @chooserText.should.not.include "Special free user tool" 43 | @chooserText.should.not.include "A tool only published for users on the free plan" 44 | -------------------------------------------------------------------------------- /bin/contact: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | 3 | info = """ 4 | Get contact details for all users with the specified plan, eg: 5 | $ bin/contact journalist 6 | Try redirecting the script’s output to a file, to get a CSV: 7 | $ bin/contact journalist > journalists.csv 8 | """ 9 | 10 | fs = require 'fs' 11 | {MongoClient} = require('mongodb') 12 | async = require 'async' 13 | 14 | rmQuote = (s) -> 15 | if not /^['"]/.test s 16 | return s 17 | t = s.replace /[^\\]|\\./g, (x) -> 18 | # Strip all quotes (which should be only leading and trailing) 19 | if /['"]/.test x 20 | return '' 21 | # Escape 22 | if x[0] == '\\' 23 | return x[1] 24 | return x 25 | return t 26 | 27 | dir = '../charm-secrets' 28 | file = "#{dir}/config/live/custard.yaml" 29 | yamltext = fs.readFileSync file, 'utf-8' 30 | m = yamltext.match /CU_DB:\s*(.+?)\s*($|\n)/ 31 | dbURL = rmQuote m[1] 32 | 33 | 34 | if process.argv.length != 3 35 | console.log info 36 | process.exit() 37 | 38 | plan = process.argv[2] 39 | 40 | MongoClient.connect dbURL, (err, db) -> 41 | if err 42 | throw err 43 | 44 | users = db.collection 'users' 45 | users.find({ accountLevel: plan }).toArray (err, users) -> 46 | if users.length 47 | printAsCSV 48 | shortName: "shortName" 49 | displayName: "displayName" 50 | email: ["email"] 51 | accountLevel: "accountLevel" 52 | async.eachSeries users, (user, done) -> 53 | printAsCSV user 54 | done() 55 | , process.exit 56 | else 57 | console.log "No users found with the account level “#{plan}”" 58 | process.exit() 59 | 60 | printAsCSV = (user) -> 61 | console.log """ 62 | "#{user.shortName}","#{user.displayName.replace '"', '""'}","#{user.email[0]}","#{user.accountLevel}" 63 | """ 64 | -------------------------------------------------------------------------------- /bin/whbox: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | fs = require 'fs' 3 | {MongoClient} = require('mongodb') 4 | async = require 'async' 5 | 6 | rmQuote = (s) -> 7 | if not /^['"]/.test s 8 | return s 9 | t = s.replace /[^\\]|\\./g, (x) -> 10 | # Strip all quotes (which should be only leading and trailing) 11 | if /['"]/.test x 12 | return '' 13 | # Escape 14 | if x[0] == '\\' 15 | return x[1] 16 | return x 17 | return t 18 | 19 | dir = '../charm-secrets' 20 | file = "#{dir}/config/live/custard.yaml" 21 | yamltext = fs.readFileSync file, 'utf-8' 22 | m = yamltext.match /CU_DB:\s*(.+?)\s*($|\n)/ 23 | dbURL = m[1] 24 | 25 | dbURL = rmQuote dbURL 26 | if /url/.test process.env.WHBOX_DEBUG 27 | console.warn "using #{dbURL} from #{file}" 28 | 29 | MongoClient.connect dbURL, (err, db) -> 30 | if err 31 | throw err 32 | boxes = db.collection 'boxes' 33 | name = process.argv[2] 34 | boxes.find({name: name}).toArray (err, boxes) -> 35 | async.eachSeries boxes, (box, cb) -> 36 | if null == box 37 | return 38 | console.log "ssh #{box.name}@#{box.server}" 39 | console.log "https://#{box.server}/#{box.name}/#{box.boxJSON.publish_token}/http/" 40 | console.log "#{box.name}:x:#{box.uid}:10000::/home:/bin/bash" 41 | console.log "users: #{box.users}" 42 | datasets = db.collection 'datasets' 43 | datasets.find({$or: [{box:box.name},{"views.box":box.name}]}).toArray (err, datasets) -> 44 | for dataset in datasets 45 | if not dataset 46 | return 47 | if dataset.box == box.name 48 | console.log "DATASET" 49 | else 50 | console.log "VIEW" 51 | console.log "dataset state: #{dataset.state}" 52 | console.log dataset 53 | cb() 54 | , -> 55 | process.exit() 56 | 57 | 58 | -------------------------------------------------------------------------------- /server/code/model/subscription.coffee: -------------------------------------------------------------------------------- 1 | request = require 'request' 2 | xml2js = require 'xml2js' 3 | 4 | class exports.Subscription 5 | constructor: (obj) -> 6 | for k of obj 7 | @[k] = obj[k] 8 | 9 | upgrade: (recurlyPlan, callback) -> 10 | request.put 11 | uri: "https://#{process.env.RECURLY_API_KEY}:@#{process.env.RECURLY_DOMAIN}.recurly.com/v2/subscriptions/#{@uuid}/" 12 | strictSSL: true 13 | headers: 14 | 'Accept': 'application/xml' 15 | 'Content-Type': 'application/xml; charset=utf-8' 16 | body: "now#{recurlyPlan}" 17 | , (err, subResp, body) -> 18 | if err? 19 | return callback {error: err}, null 20 | else if subResp.statusCode isnt 200 21 | return callback { statusCode: subResp.statusCode, error: subResp.body }, null 22 | return callback null, subResp 23 | 24 | @getRecurlyResult: (token, callback) -> 25 | #TODO: check for valid plan code & recurlyAccount 26 | request.get 27 | uri: "https://#{process.env.RECURLY_API_KEY}:@api.recurly.com/v2/recurly_js/result/#{token}" 28 | strictSSL: true 29 | headers: 30 | 'Accept': 'application/xml' 31 | 'Content-Type': 'application/xml; charset=utf-8' 32 | , (err, recurlyResp, body) -> 33 | if err? 34 | callback {error: err}, null 35 | else if recurlyResp.statusCode isnt 200 36 | callback {statusCode: recurlyResp.statusCode, error: recurlyResp.body}, null 37 | else 38 | parser = new xml2js.Parser 39 | ignoreAttrs: true 40 | explicitArray: false 41 | parser.parseString recurlyResp.body, (err, obj) -> 42 | if err? 43 | callback {error: err}, null 44 | else 45 | callback null, obj 46 | -------------------------------------------------------------------------------- /client/template/create-profile.eco: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 | 17 |

18 |

19 | 20 | 21 |

22 |

23 | 24 | 25 |

26 |

27 | 28 | 29 |

30 |

31 | 32 | 33 |

34 |

35 | 36 |

37 |
38 |
39 | -------------------------------------------------------------------------------- /shared/code/page-titles.coffee: -------------------------------------------------------------------------------- 1 | PageTitles = 2 | 'help-home': 3 | heading: "Docs" 4 | title: "Help | QuickCode" 5 | 'help-corporate': 6 | heading: 'Corporate FAQs' 7 | title: "Corporate FAQs | Help | QuickCode" 8 | 'help-whats-new': 9 | heading: "What's new?" 10 | title: "What's new? | Help | QuickCode" 11 | 'help-developer': 12 | heading: "Developer FAQs" 13 | title: "Developer FAQs | Help | QuickCode" 14 | 'help-zig': 15 | heading: "ZIG" 16 | title: "Zarino Interface Guidelines | Help | QuickCode" 17 | 'help-upload-and-summarise': 18 | heading: 'Upload and summarise a spreadsheet of data' 19 | title: "Upload and Summarise | Help | QuickCode" 20 | 'help-code-in-your-browser': 21 | heading: 'Code a script in your browser' 22 | title: "Code a Script | Help | QuickCode" 23 | 'help-make-your-own-tool': 24 | heading: 'Make your own tool with HTML, JavaScript & Python' 25 | title: "Make your own tool | Help | QuickCode" 26 | 'terms': 27 | heading: 'Terms & Conditions' 28 | title: "Terms & Conditions | QuickCode" 29 | 'terms-enterprise-agreement': 30 | heading: 'QuickCode Enterprise Agreement' 31 | title: "Enterprise Agreement | QuickCode" 32 | 'pricing': 33 | heading: 'Pricing' 34 | title: "Pricing | QuickCode" 35 | 'sign-up': 36 | heading: 'Sign up' 37 | title: "Sign up | QuickCode" 38 | 'set-password': 39 | heading: 'Set your password' 40 | title: 'Set password | QuickCode' 41 | 'reset-password': 42 | heading: 'Request a password reset link' 43 | title: 'Reset password | QuickCode' 44 | 'create-profile': 45 | heading: 'Create Profile' 46 | title: 'Create Profile | QuickCode' 47 | '404': 48 | heading: '404: Not Found' 49 | title: "404 | QuickCode" 50 | 51 | if exports? 52 | exports.PageTitles = PageTitles 53 | else 54 | window.PageTitles = PageTitles 55 | -------------------------------------------------------------------------------- /shared/vendor/js/jquery.tinysort.charorder.min.js: -------------------------------------------------------------------------------- 1 | /*! TinySort CharOrder 1.1.2 2 | * Copyright (c) 2008-2013 Ron Valstar http://tinysort.sjeiti.com/ 3 | * License: 4 | * MIT: http://www.opensource.org/licenses/mit-license.php 5 | * GPL: http://www.gnu.org/licenses/gpl.html 6 | */ 7 | !function(a,b){"use strict";for(var c,d,e,f,g,h,i=[],j=9472,k={},l=String.fromCharCode,m=Math.min,n=null,o=b.plugin.indexOf,p=32,q=l(p),r=255;r>p;p++,q=l(p).toLowerCase())-1===o.call(i,q)&&i.push(q);i.sort(),b.charorder={id:"TinySort CharOrder",version:"1.1.2",requires:"TinySort 1.4",copyright:"Copyright (c) 2008-2013 Ron Valstar",uri:"http://tinysort.sjeiti.com/",licensed:{MIT:"http://www.opensource.org/licenses/mit-license.php",GPL:"http://www.gnu.org/licenses/gpl.html"}},b.defaults.charOrder=c,b.plugin(function(b){if(g=b,h="asc"==g.order?1:-1,g.charOrder!=c)if(c=g.charOrder,g.charOrder){d=i.slice(0),e=!1;for(var m,p,q=[],r=function(a,b){q.push(b),k[g.cases?a:a.toLowerCase()]=b},s="",t="z",u=c.length,v=0;u>v;v++){var w=c[v],x=w.charCodeAt(),y=x>96&&123>x;if(!y)if("["==w){var z=q.length,A=z?q[z-1]:t,B=c.substr(v+1).match(/[^\]]*/)[0],C=B.match(/{[^}]*}/g);if(C)for(m=0,p=C.length;p>m;m++){var D=C[m];v+=D.length,B=B.replace(D,""),r(D.replace(/[{}]/g,""),A),e=!0}for(m=0,p=B.length;p>m;m++)r(A,B[m]);v+=B.length+1}else if("{"==w){var E=c.substr(v+1).match(/[^}]*/)[0];r(E,l(j++)),v+=E.length+1,e=!0}else q.push(w);if(q.length&&(y||v===u-1)){var F=q.join("");s+=F,a.each(F.split(""),function(a,b){d.splice(o.call(d,b),1)});var G=q.slice(0);G.splice(0,0,o.call(d,t)+1,0),Array.prototype.splice.apply(d,G),q.length=0}v+1===u?f=new RegExp("["+s+"]","gi"):y&&(t=w)}}else e=!1,j=9472,k={},f=d=n},function(b,c,i,j){if(!b&&g.charOrder&&(e&&a.each(k,function(a,b){c=c.replace(a,b),i=i.replace(a,b)}),c.match(f)!==n||i.match(f)!==n))for(var l=0,p=m(c.length,i.length);p>l;l++){var q=o.call(d,c[l]),r=o.call(d,i[l]);if(j=h*(r>q?-1:q>r?1:0))break}return j})}(jQuery,jQuery.tinysort); -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -x 4 | set -e 5 | set -u 6 | 7 | # TODO(pwaller): Working directory = dir of script 8 | 9 | docker build -t custard . 10 | 11 | # source ../../swops-secret/keys.sh 12 | export CU_DB=mongodb://localhost:27017/cu-test 13 | export CU_BOX_SERVER=scraperiwiki.example.com 14 | export CU_SESSION_SECRET=foo 15 | export CU_TOOLS_DIR=/var/tmp/tools 16 | # export CU_BOX_SERVER= # defaults to value from DB. 17 | export CU_SENDGRID_USER=foo@example.com 18 | export CU_SENDGRID_PASS=foo@example.com 19 | export CU_MAILCHIMP_API_KEY=foo 20 | export CU_MAILCHIMP_LIST_ID=foo 21 | 22 | # When using this variable, don't write "$ENVS", write $ENVS. 23 | # Word splitting is intentional. 24 | ENVS="$(env | grep CU_ | sed 's/^/-e /')" 25 | 26 | if ! docker inspect custard-data &> /dev/null 27 | then 28 | 29 | echo "custard-data doesn't exist, populating it" 30 | 31 | cp ../package.json ./custard-data-image/package.json 32 | sed -i .bak '/cake build/d' ./custard-data-image/package.json 33 | 34 | docker build -t custard-data-image custard-data-image 35 | 36 | docker run \ 37 | --name custard-data \ 38 | -w /data \ 39 | -v /data/node_modules \ 40 | custard-data-image \ 41 | npm install --unsafe-perm 42 | else 43 | echo "Reusing existing custard-data" 44 | fi 45 | 46 | cd .. 47 | 48 | echo LS: 49 | ls -l /tang/repo/ 50 | echo PWD: 51 | pwd 52 | echo LS PWD: 53 | ls -l $PWD 54 | 55 | NAME=tang-run-${TANG_SHA} 56 | 57 | # Note: This will all change when we do DIND 58 | # (docker in docker). Note that the volume mounts only work for DOD. 59 | time docker -D run -t $ENVS \ 60 | --name $NAME \ 61 | -w /opt/custard \ 62 | --volumes-from custard-data \ 63 | -v /var/lib$PWD:/opt/custard \ 64 | -v /db:/db \ 65 | custard \ 66 | docker/start.sh 67 | 68 | S=$(docker wait $NAME) 69 | 70 | echo Deleting $(docker rm $NAME) 71 | 72 | echo "Docker exited, status: $S" 73 | exit $S 74 | -------------------------------------------------------------------------------- /client/code/view/dataset/row.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.DatasetRow extends Backbone.View 2 | tagName: 'tr' 3 | className: 'dataset' 4 | attributes: -> 5 | 'data-box': @model.get 'box' 6 | 7 | events: 8 | 'click .hide': 'hideDataset' 9 | 'click .unhide': 'unhideDataset' 10 | 'click': 'visitDataset' 11 | 12 | initialize: (options) -> 13 | @options = options || {} 14 | 15 | # Check whether a `clickable` option has been passed to 16 | # this view's constructor (eg: by Cu.View.Dashboard). 17 | if @options.clickable? 18 | @clickable = @options.clickable 19 | 20 | @model.on 'change', @render, this 21 | @model.on 'destroy', @destroy, this 22 | 23 | render: -> 24 | if @model.get('state') is 'deleted' 25 | @$el.addClass 'deleted' 26 | @$el.html JST['dataset-row-deleted'] 27 | else 28 | toolManifest = @model.get('tool')?.get('manifest') 29 | @$el.removeClass 'deleted' 30 | if @model.get('status')?.type is 'error' 31 | @$el.addClass 'error' 32 | @$el.html JST['dataset-row'] 33 | dataset: @model.toJSON() 34 | statusUpdatedHuman: @model.statusUpdatedHuman() 35 | datasetCreatedHuman: @model.datasetCreatedHuman() 36 | swatchColor: toolManifest?.color 37 | @ 38 | 39 | destroy: => 40 | @remove() 41 | 42 | visitDataset: => 43 | if @clickable 44 | app.navigate "/dataset/#{@model.get 'box'}", trigger: true 45 | 46 | hideDataset: (e) -> 47 | e.preventDefault() 48 | e.stopPropagation() 49 | 50 | @model.destroy() 51 | @timeout = setTimeout(@destroy, 5 * 60000) 52 | 53 | unhideDataset: (e) -> 54 | e.preventDefault() 55 | e.stopPropagation() 56 | 57 | clearTimeout(@timeout) 58 | @model.recover() 59 | 60 | # Whether or not to make this dataset row clickable. 61 | # (Cu.View.Dashboard sets this to false, because 62 | # dataset rows there shouldn't be clickable). 63 | clickable: true 64 | -------------------------------------------------------------------------------- /test/integration/signup.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, base_url, home_url} = require './helper' 4 | 5 | describe 'Sign up', -> 6 | 7 | before (done) -> 8 | browser.deleteAllCookies done 9 | 10 | context 'when I select the Free plan on the pricing page', -> 11 | before (done) -> 12 | browser.get "#{base_url}/pricing/", done 13 | 14 | before (done) -> 15 | wd40.click '.plan.freetrial a', done 16 | 17 | it 'has "Free Trial" and "Sign Up" in the page title', (done) -> 18 | browser.title (err, title) -> 19 | title.should.match /Free Trial/g 20 | title.should.match /Sign Up/g 21 | done() 22 | 23 | it 'the newsletter checkbox is ticked by default', (done) -> 24 | wd40.elementByCss '#emailMarketing', (err, element) -> 25 | should.exist element 26 | element.getAttribute 'checked', (err, checked) -> 27 | should.exist checked 28 | checked.should.be.ok # asserts truthfulness 29 | done() 30 | 31 | context 'when I enter my details and click go', -> 32 | before (done) -> 33 | wd40.fill '#displayName', 'Tabatha Testington', -> 34 | # we clear the short name, which has been prefilled with a made up one for us 35 | # XXX test the text of the prefilled one is good 36 | wd40.clear '#shortName', -> 37 | wd40.fill '#shortName', 'tabbytest', -> 38 | wd40.fill '#email', 'tabby@example.org', -> 39 | wd40.click '#acceptedTerms', -> 40 | wd40.click '#go', done 41 | 42 | it 'it says thanks', (done) -> 43 | wd40.waitForText 'Thankyou for signing up', 10000, done 44 | 45 | it 'it tells me to check my emails', (done) -> 46 | wd40.waitForText 'check your email', done 47 | 48 | it 'it takes me to the /thankyou page', (done) -> 49 | wd40.waitForMatchingURL /[/]thankyou/, done 50 | 51 | -------------------------------------------------------------------------------- /client/code/view/error.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.Error extends Backbone.View 2 | className: "error-page" 3 | 4 | initialize: (options) -> 5 | @options = options || {} 6 | 7 | render: -> 8 | @el.innerHTML = """

#{@options.title} #{@options?.message}

""" 9 | @ 10 | 11 | class Cu.View.ErrorAlert extends Backbone.View 12 | 13 | initialize: (options) -> 14 | @options = options || {} 15 | 16 | $(document).ajaxError @displayAJAXError 17 | Backbone.on 'error', @onError, this 18 | 19 | render: (errorHTML) -> 20 | $('#fullscreen').css 'top': '173px' 21 | @$el.find('span').html errorHTML 22 | # http://stackoverflow.com/a/1145297/2653738 23 | $("html, body").animate scrollTop: 0 24 | @$el.fadeOut 100, => 25 | @$el.fadeIn 300 26 | @ 27 | 28 | hide: => 29 | @$el.hide() 30 | 31 | onError: (model, xhr, options) => 32 | setTimeout => 33 | # ignore errors if the user's about to leave the page 34 | return false if window.aboutToClose 35 | 36 | try 37 | @render JSON.parse(xhr.responseText).error 38 | catch error 39 | @render xhr.responseText 40 | , 500 41 | 42 | displayAJAXError: (event, jqXHR, ajaxSettings, thrownError) => 43 | setTimeout => 44 | # ignore errors if the user's about to leave the page 45 | return false if window.aboutToClose 46 | 47 | message = "xhr.statusText: #{jqXHR.statusText}" 48 | # Possibly translate the error into a more helpful error 49 | # message to display. 50 | # 0 is connection refused or ajax request aborted (eg: page reload) 51 | # 502 is Bad Gateway which nginx will serve when custard is 52 | # basically dead (or starting up). 53 | # 504 is Gateway Timeout, this will happen if a custard request times out 54 | if jqXHR?.status in [0, 502, 504] 55 | message = "Sorry! We couldn't connect to the server, please try again." 56 | return @render message 57 | , 500 58 | -------------------------------------------------------------------------------- /test/integration/view.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, loginAndGo} = require './helper' 4 | 5 | describe 'View', -> 6 | randomname = "Prune graph number #{Math.random()}" 7 | 8 | before (done) -> 9 | loginAndGo "ehg", "testing", "/datasets", done 10 | 11 | context 'when I click on a Prune dataset then the graph of prunes view', -> 12 | before (done) -> 13 | wd40.elementByPartialLinkText 'Prune', (err, link) -> 14 | link.click done 15 | 16 | before (done) -> 17 | wd40.waitForVisibleByCss '#toolbar .tool[data-toolname="prune-graph"] .tool-icon', -> 18 | wd40.click '#toolbar .tool[data-toolname="prune-graph"] .tool-icon', done 19 | 20 | it 'takes me to the Graph of Prunes page', (done) -> 21 | wd40.waitForMatchingURL /\/view\/(\w+)/, (err, url) -> 22 | done() 23 | 24 | it 'there is a custom-named "Data Scientist\'s Report" tool', (done) -> 25 | wd40.getText '#dataset-tools', (err, text) -> 26 | text.replace(/\s+/g, ' ').toLowerCase().should.include "data scientist's report" 27 | done() 28 | 29 | context 'when I click the "hide" link on the "Code a prune" tool', -> 30 | before (done) -> 31 | wd40.elementByCss '#toolbar .tool[data-toolname="prune-graph"]', (err, el) -> 32 | el.elementByCss '.dropdown-toggle', (err, optionsToggle) -> 33 | optionsToggle.click (err) -> 34 | wd40.click '#tool-options-menu .hide-tool', done 35 | 36 | it "I'm redirected to the dataset's default tool", (done) -> 37 | wd40.waitForMatchingURL /dataset[/]\w+([/]settings)?[/]?$/, done 38 | 39 | it 'And the "Code a prune" tool is no longer visible', (done) -> 40 | # we use browser, and not wd40, here because wd40 41 | # would timeout waiting for the element to appear 42 | browser.elementByCss '#toolbar .tool[data-toolname="prune-graph"]', (err, el) -> 43 | should.not.exist el 44 | done() 45 | -------------------------------------------------------------------------------- /client/code/view/tool/list.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.ToolList extends Backbone.View 2 | id: 'chooser' 3 | 4 | events: 5 | 'click .close': 'closeChooser' 6 | 'click': 'closeChooser' 7 | 8 | initialize: (options) -> 9 | @options = options || {}; 10 | 11 | app.tools().on 'fetched', @addTools, @ 12 | 13 | Backbone.on 'error', => 14 | # close chooser on errors, 15 | # so user can see red alert bar behind 16 | @closeChooser() 17 | 18 | $(window).on 'keyup', (e) => 19 | if e.which == 27 20 | @closeChooser() 21 | 22 | render: -> 23 | @$el.hide().append('×') 24 | headerView = new Cu.View.ToolListHeader {type: @options.type} 25 | @$el.append headerView.render().el 26 | @row = $('
').appendTo @$el 27 | @row.wrap('
') 28 | @addTools() if app.tools().length 29 | @$el.fadeIn 200 30 | return this 31 | 32 | addTools: -> 33 | @$el.remove('.tool') 34 | app.tools().each @addTool 35 | 36 | addTool: (tool) => 37 | if @options.type is 'importers' and tool.get('type') is 'importer' 38 | view = new Cu.View.AppTile model: tool 39 | view.on 'install:failed', => 40 | @closeChooser false 41 | , @ 42 | @row.append view.render().el 43 | else if @options.type isnt 'importers' and tool.get('type') isnt 'importer' 44 | view = new Cu.View.PluginTile { model: tool, dataset: @options.dataset } 45 | @row.append view.render().el 46 | 47 | closeChooser: (navigate=true) -> 48 | @$el.fadeOut 200, => 49 | # TODO: we want to go back to the last page, but 50 | # on our site only window.history.back() will screw up stuff 51 | if @options.type is 'importers' 52 | if navigate then app.navigate '/datasets', trigger: Backbone.history.routeCount < 2 53 | else 54 | if navigate then app.navigate "/dataset/#{@options.dataset.get 'box'}", trigger: Backbone.history.routeCount < 2 55 | $(window).off('keyup') 56 | -------------------------------------------------------------------------------- /migration/2013-08-symlink-migration/should-migrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd /ebs/home/$1/tool 2> /dev/null || { echo "Box $1 doesn't have a tool."; exit 1; } 5 | 6 | if [ "$3" != "code-scraper-in-browser" ] && [ -e ../.gitconfig ] && grep -qv "Anon" ../.gitconfig; then 7 | echo "-----------------------------------" 8 | echo "Box $1 has a customised .gitconfig:" 9 | cat ../.gitconfig 10 | exit 1 11 | fi 12 | 13 | if git branch | grep -q -v master 14 | then 15 | echo "Box $1 has a unrecognised git branch" 16 | exit 1 17 | fi 18 | 19 | count=$(git log origin/master..HEAD | wc -l) 20 | if [ $count -ne 0 ] 21 | then 22 | echo "Box $1 has a extra commits" 23 | exit 1 24 | fi 25 | 26 | IGNORE_FILES="egrep -v tool-update.log | egrep -v '^(!!|\?\?) http/' | egrep -v '\.pyc$'" 27 | 28 | count=$(git status --porcelain --ignored | eval "$IGNORE_FILES" | wc -l) 29 | if [ $count -ne 0 ] 30 | then 31 | echo "Box $1 has extra stuff" 32 | git status --porcelain --ignored 33 | echo "##########################" 34 | exit 1 35 | fi 36 | 37 | count=$(git stash list | wc -l) 38 | if [ $count -ne 0 ] 39 | then 40 | echo "Box $1 has a stash" 41 | exit 1 42 | fi 43 | 44 | IGNORE_FETCHREFS="egrep -v '^\\s+fetch = \\+refs/heads/master:refs/remotes/origin/master$' | egrep -v '^\\s+fetch = \\+refs/heads/\\*:refs/remotes/origin/\\*$' | egrep -v '^\\s+url ='" 45 | 46 | 47 | cat .git/config | eval "$IGNORE_FETCHREFS" > /tmp/sw-git-tool 48 | cat /tools/$3/.git/config | eval "$IGNORE_FETCHREFS" > /tmp/sw-git-tool-original 49 | 50 | # .git/config MD5? 51 | md5sum1="$(md5sum /tmp/sw-git-tool | awk '{print $1}')" 52 | md5sum2="$(md5sum /tmp/sw-git-tool-original | awk '{print $1}')" 53 | if [[ "$md5sum1" != "$md5sum2" ]] 54 | then 55 | echo "------------------------------" 56 | echo "Box $1 has a git config change" 57 | echo "'$md5sum1'" 58 | echo "'$md5sum2'" 59 | diff -u /tmp/sw-git-tool-original /tmp/sw-git-tool 60 | echo "------------------------------" 61 | exit 1 62 | fi 63 | 64 | # compare .git 65 | 66 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {spawn, exec} = require 'child_process' 2 | fs = require 'fs' 3 | 4 | glob = require 'glob' 5 | which = require 'which' 6 | async = require 'async' 7 | 8 | process.title = 'cake ' + process.argv[2..].join ' ' 9 | 10 | pkg = JSON.parse fs.readFileSync('./package.json') 11 | testCmd = pkg.scripts.test 12 | startCmd = pkg.scripts.start 13 | 14 | log = (message, explanation) -> 15 | console.log "#{message} #{explanation or ''}" 16 | 17 | compile = (outputDir, inputDir, watch, callback) -> 18 | options = ['-c','-b'] 19 | options.push('-w') if watch is true 20 | options = options.concat ['-o', outputDir, inputDir] 21 | cmd = which.sync 'coffee' 22 | coffee = spawn cmd, options 23 | coffee.stdout.pipe process.stdout 24 | coffee.stderr.pipe process.stderr 25 | coffee.on 'exit', (status) -> callback?() 26 | coffee 27 | 28 | # Compiles app.coffee and src directory to the app directory 29 | build = (watch, callback) -> 30 | async.parallel [ 31 | (cb) -> compile('server/js', 'server/code', watch, cb), 32 | (cb) -> compile('shared/js', 'shared/code', watch, cb) 33 | ] 34 | 35 | task 'clean', -> 36 | console.log "Cleaning database and inserting fixtures" 37 | exec 'test/cleaner.coffee' 38 | 39 | task 'build', -> 40 | build false, -> 41 | process.exit 0 42 | 43 | task 'test', 'Run unit tests', -> 44 | build -> test process.argv[3..] 45 | 46 | task 'dev', 'start dev env', -> 47 | process.env.NODE_ENV = 'testing' 48 | nodemon = spawn 'nodemon', ['--ignore', './test', './server/code/index.coffee'] 49 | nodemon.stdout.pipe process.stdout 50 | nodemon.stderr.pipe process.stderr 51 | 52 | Selenium = -> 53 | glob "selenium-server-standalone-2.*.jar", null, (err, files) -> 54 | jarfile = files[files.length-1] 55 | se = spawn 'java', ['-jar', jarfile, 56 | '-Dwebdriver.chrome.driver=chromedriver'] 57 | se.stdout.pipe process.stdout 58 | se.stderr.pipe process.stderr 59 | log 'Selenium started' 60 | 61 | task 'se', 'start selenium', Selenium 62 | task 'Se', 'start Selenium', Selenium 63 | -------------------------------------------------------------------------------- /client/code/view/profile/reset-password.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.ResetPassword extends Backbone.View 2 | className: "reset-password" 3 | 4 | events: 5 | 'click #go': 'sendResetEmail' 6 | 7 | render: -> 8 | @el.innerHTML = JST['reset-password'] window.user?.real 9 | $('#forgotten-shortname', @$el).popover( 10 | title: "No problem!" 11 | content: 'Email us for a reset link: hello@quickcode.io' 12 | html: true 13 | placement: 'right' 14 | ).on('click', (e) -> 15 | e.preventDefault() 16 | e.stopPropagation() 17 | ).css('cursor', 'pointer') 18 | @ 19 | 20 | sendResetEmail: (e) -> 21 | e.preventDefault() 22 | query = $('#query').val() 23 | @$el.find('.alert').remove() 24 | @$el.find('.control-group').removeClass('error') 25 | if query == '' 26 | @$el.find('.control-group').addClass('error').children('label').text('You must supply a username:').next().focus() 27 | else 28 | $('#go').attr('disabled', true).addClass('loading') 29 | $.ajax 30 | url: "#{location.protocol}//#{location.host}/api/user/reset-password/" 31 | type: 'POST' 32 | data: 33 | query: query 34 | dataType: 'json' 35 | success: (data) => 36 | $('form', @$el).prepend """
Password reset link sent. Please check your emails.
""" 37 | $('#go').attr('disabled', false).removeClass('loading') 38 | error: (jqxhr, textStatus, errorThrown) => 39 | if jqxhr.status == 404 40 | msg = """
Hmmm. That username could not be found.
""" 41 | else 42 | msg = """
Hmmm. Something went wrong. Email hello@quickcode.io and we’ll email you a password reset link manually.
""" 43 | $('form', @$el).prepend msg 44 | $('#go').attr('disabled', false).removeClass('loading') 45 | -------------------------------------------------------------------------------- /server/template/base_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | QuickCode 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | -------------------------------------------------------------------------------- /server/template/login.html: -------------------------------------------------------------------------------- 1 | <% include base_header.html %> 2 | 3 | 43 | 49 | 50 | <% include base_footer.html %> 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custard", 3 | "version": "0.1.0", 4 | "private": true, 5 | "bugs": "https://github.com/scraperwiki/custard/issues", 6 | "license": "GNU Affero General Public License", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/scraperwiki/custard.git" 10 | }, 11 | "dependencies": { 12 | "async": "~0.2.6", 13 | "bcrypt": "~0.7.3", 14 | "coffee-script": "~1.7.0", 15 | "connect": "~2.15.0", 16 | "connect-assets": "~2.5.5", 17 | "connect-flash": "~0.1.0", 18 | "connect-mongo": "~0.4.2", 19 | "cookie-parser": "^1.0.1", 20 | "eco": "~1.1.0-rc-3", 21 | "ejs": "~0.8.3", 22 | "exceptional-node": "~0.1.1", 23 | "express": "~4.1.1", 24 | "express-session": "^1.0.4", 25 | "glob": "~3.2.6", 26 | "ident-express": "*", 27 | "mailchimp": "~1.1.0", 28 | "moment": "~2.6.0", 29 | "mongoose": "~3.8.25", 30 | "morgan": "^1.0.1", 31 | "nodemailer": "~0.3.43", 32 | "nodemon": "~1.0.17", 33 | "passport": "~0.1.15", 34 | "passport-local": "~0.1.6", 35 | "qs": "~0.5.5", 36 | "rand-token": "~0.2.1", 37 | "request": "~2.21", 38 | "rimraf": "~2.1.1", 39 | "serve-favicon": "^2.0.0", 40 | "supertest": "^0.12.0", 41 | "throttler-express": "git+https://github.com/scraperwiki/throttler-express.git", 42 | "underscore": "~1.4.4", 43 | "uuid": "~1.4.0", 44 | "which": "*", 45 | "xml2js": "~0.2.6" 46 | }, 47 | "scripts": { 48 | "install": "node_modules/coffee-script/bin/cake build", 49 | "start": ". ./activate && cake dev", 50 | "test": "bash -c '. ./activate && { mocha test/unit ; }'" 51 | }, 52 | "devDependencies": { 53 | "mocha": "~1.18.0", 54 | "mkdirp": "~0.3.4", 55 | "should": "~3.1.2", 56 | "sinon": "~1.10.0", 57 | "snockets": "~1.3.8", 58 | "backbone": "1.1.0", 59 | "optimist": "~0.5.2", 60 | "jquery": "~1.7.3", 61 | "pow-mongodb-fixtures": "*", 62 | "backbone-relational": "~0.8.0", 63 | "jsdom": "~0.10.1", 64 | "wd": "~0.2.21", 65 | "wd40": "~0.1.0", 66 | "mongodb": "~2.2.19", 67 | "sqlite3": "~2.1.7" 68 | }, 69 | "engines": { 70 | "node": "~0.10.x", 71 | "npm": "1.2.x" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /shared/vendor/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*jshint eqnull:true */ 2 | /*! 3 | * jQuery Cookie Plugin v1.2 4 | * https://github.com/carhartl/jquery-cookie 5 | * 6 | * Copyright 2011, Klaus Hartl 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://www.opensource.org/licenses/mit-license.php 9 | * http://www.opensource.org/licenses/GPL-2.0 10 | */ 11 | (function ($, document, undefined) { 12 | 13 | var pluses = /\+/g; 14 | 15 | function raw(s) { 16 | return s; 17 | } 18 | 19 | function decoded(s) { 20 | return decodeURIComponent(s.replace(pluses, ' ')); 21 | } 22 | 23 | $.cookie = function (key, value, options) { 24 | 25 | // key and at least value given, set cookie... 26 | if (value !== undefined && !/Object/.test(Object.prototype.toString.call(value))) { 27 | options = $.extend({}, $.cookie.defaults, options); 28 | 29 | if (value === null) { 30 | options.expires = -1; 31 | } 32 | 33 | if (typeof options.expires === 'number') { 34 | var days = options.expires, t = options.expires = new Date(); 35 | t.setDate(t.getDate() + days); 36 | } 37 | 38 | value = String(value); 39 | 40 | return (document.cookie = [ 41 | encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value), 42 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 43 | options.path ? '; path=' + options.path : '', 44 | options.domain ? '; domain=' + options.domain : '', 45 | options.secure ? '; secure' : '' 46 | ].join('')); 47 | } 48 | 49 | // key and possibly options given, get cookie... 50 | options = value || $.cookie.defaults || {}; 51 | var decode = options.raw ? raw : decoded; 52 | var cookies = document.cookie.split('; '); 53 | for (var i = 0, parts; (parts = cookies[i] && cookies[i].split('=')); i++) { 54 | if (decode(parts.shift()) === key) { 55 | return decode(parts.join('=')); 56 | } 57 | } 58 | 59 | return null; 60 | }; 61 | 62 | $.cookie.defaults = {}; 63 | 64 | $.removeCookie = function (key, options) { 65 | if ($.cookie(key, options) !== null) { 66 | $.cookie(key, null, options); 67 | return true; 68 | } 69 | return false; 70 | }; 71 | 72 | })(jQuery, document); 73 | -------------------------------------------------------------------------------- /server/code/plans.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataservices-ec2": { 3 | "boxServer": "ds-ec2.scraperwiki.com", 4 | "maxDiskSpaceMB": 4000, 5 | "maxHourCPUPerDay": 2.0, 6 | "maxNumDatasets": 100, 7 | "$": true 8 | }, 9 | "free": { 10 | "boxServer": "free-ec2.scraperwiki.com", 11 | "maxDiskSpaceMB": 8, 12 | "maxHourCPUPerDay": 0.5, 13 | "maxNumDatasets": 3, 14 | "$": false 15 | }, 16 | "free-trial": { 17 | "boxServer": "free-ec2.scraperwiki.com", 18 | "maxDiskSpaceMB": 8, 19 | "maxHourCPUPerDay": 0.5, 20 | "maxNumDatasets": 3, 21 | "$": false 22 | }, 23 | "grandfather-ec2": { 24 | "boxServer": "premium.scraperwiki.com", 25 | "maxDiskSpaceMB": 8000, 26 | "maxHourCPUPerDay": 0.5, 27 | "maxNumDatasets": 100, 28 | "$": false 29 | }, 30 | "xlarge-ec2": { 31 | "boxServer": "premium.scraperwiki.com", 32 | "maxDiskSpaceMB": 512, 33 | "maxHourCPUPerDay": 0.5, 34 | "maxNumDatasets": 999, 35 | "$": true 36 | }, 37 | "large-ec2": { 38 | "boxServer": "premium.scraperwiki.com", 39 | "maxDiskSpaceMB": 256, 40 | "maxHourCPUPerDay": 0.5, 41 | "maxNumDatasets": 100, 42 | "$": true 43 | }, 44 | "medium-ec2": { 45 | "boxServer": "premium.scraperwiki.com", 46 | "maxDiskSpaceMB": 64, 47 | "maxHourCPUPerDay": 0.5, 48 | "maxNumDatasets": 10, 49 | "$": true 50 | }, 51 | "journalist": { 52 | "boxServer": "premium.scraperwiki.com", 53 | "maxDiskSpaceMB": 128, 54 | "maxHourCPUPerDay": 0.5, 55 | "maxNumDatasets": 20, 56 | "$": false 57 | }, 58 | "opendata": { 59 | "boxServer": "premium.scraperwiki.com", 60 | "maxDiskSpaceMB": 128, 61 | "maxHourCPUPerDay": 0.5, 62 | "maxNumDatasets": 20, 63 | "$": false 64 | }, 65 | "cobalt-a-dev": { 66 | "comment": "This plan is here for development purposes", 67 | "boxServer": "cobalt-p.scraperwiki.com", 68 | "maxDiskSpaceMB": 4000, 69 | "maxHourCPUPerDay": 2.0, 70 | "maxNumDatasets": 100, 71 | "$": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /client/code/view/subscribe.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.Subscribe extends Backbone.View 2 | className: 'subscribe' 3 | 4 | initialize: (options) -> 5 | @options = options || {} 6 | 7 | render: -> 8 | @el.innerHTML = JST['subscribe'] @options 9 | $.getScript "/vendor/js/recurly.js", => 10 | splitDisplayName = window.user.effective.displayName.split ' ' 11 | Recurly.config 12 | subdomain: window.recurlyDomain 13 | currency: 'USD' 14 | country: 'GB' 15 | VATPercent: '20' 16 | Recurly.buildSubscriptionForm 17 | target: '#recurly-subscribe' 18 | accountCode: window.user.effective.recurlyAccount 19 | planCode: @options.plan 20 | distinguishContactFromBillingInfo: true 21 | collectCompany: true 22 | enableCoupons: true 23 | enableAddOns: false 24 | acceptedCards: ['mastercard', 'visa'] 25 | account: 26 | firstName: splitDisplayName[0] 27 | lastName: splitDisplayName[splitDisplayName.length - 1] 28 | email: window.user.effective.email[0] 29 | billingInfo: 30 | firstName: splitDisplayName[0] 31 | lastName: splitDisplayName[splitDisplayName.length - 1] 32 | signature: @options.signature 33 | beforeInject: @beforeInject 34 | successHandler: @onSuccessfulTransaction 35 | 36 | onSuccessfulTransaction: (token) -> 37 | shortName = window.user.effective.shortName 38 | $.ajax 39 | type: 'POST' 40 | url: "/api/#{shortName}/subscription/verify" 41 | data: 42 | recurly_token: token 43 | success: (result) => 44 | # Do a window refresh here, and not a backbone navigate 45 | # because we want to get a fresh user session that has 46 | # a fresh accountLevel field (obtained from the server). 47 | window.location.href = '/thankyou' 48 | error: (err) => 49 | alert err 50 | 51 | beforeInject: -> 52 | # TODO: move to eco 53 | $('form').prepend('

Almost there! Just enter your payment details.

') 54 | $('.footer', $('form')).prepend('← Go back to pricing plans') 55 | $('.country select', $('form')).val('US').trigger('change') 56 | -------------------------------------------------------------------------------- /client/template/help-whats-new.eco: -------------------------------------------------------------------------------- 1 |
2 | The QuickCode digger 3 |

Welcome to QuickCode!

4 |

QuickCode helps you get your data, and use it in the way that you need. 5 | Here’s a sixty-second rundown of how it works.

6 |

7 | Read the docs 8 | Log in 9 |

10 |
11 | 12 |
13 |

The secret sauce: Using tools for instant wins.

14 |

QuickCode is a simple platform. All of the magic is performed by tools.

15 |

Tools are single-purpose programs which let domain experts do exactly what they need to do, without touching a line of code.

16 | Try the quick start guides 17 |
18 | 19 |
20 |

QuickCode is about datasets and tools.

21 |

Tools create, update, analyse and export data.

22 |

You can install new tools from the tool shop, tweak existing ones, or write your own.

23 | <% if window?.user?.effective: %> 24 | Create a new dataset 25 | <% else: %> 26 | Create a new dataset 27 | <% end %> 28 |
29 | 30 |
31 |

Developers! Code locally, or in your browser.

32 |

You can choose to Code in your Browser.

33 |

You can also develop in your local environment, and push code to QuickCode with Git or SSH.

34 | Read more about boxes 35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /activate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # run me with . activate 3 | 4 | OLD_PATH=$PATH 5 | OLD_PS1=$PS1 6 | OLD_NODE_PATH=$NODE_PATH 7 | PATH=$(pwd)/bin:$(pwd)/node_modules/.bin:$PATH 8 | 9 | # The default values for these environment variables below are: 10 | # a) not secret; and, 11 | # b) supposed to work with local demons (mongo) 12 | export CU_DB=mongodb://localhost:27017/cu-test 13 | export CU_BOX_SERVER=scraperwiki.example.com 14 | export CU_SESSION_SECRET=foo 15 | export CU_TOOLS_DIR=/var/tmp/tools 16 | export CU_SENDGRID_USER=foo@example.com 17 | export CU_SENDGRID_PASS=foo@example.com 18 | export CU_MAILCHIMP_API_KEY=foo 19 | export CU_MAILCHIMP_LIST_ID=foo 20 | 21 | # Fiddling with NODE_PATH appears not to be necessary. (node 22 | # picks up modules from ./node_modules, but maybe that's only 23 | # because i'm using nvm) 24 | 25 | # Add ./code to NODE_PATH, adding a separating ':' if NODE_PATH 26 | # is already set. 27 | # See http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02 28 | export NODE_PATH=$(pwd)/server/code${NODE_PATH+:}${NODE_PATH} 29 | 30 | closest () { 31 | # Searches parent directories to find the first one that contains 32 | # "$1" then prints the full path, including the appended "$1". 33 | # example use: 34 | # cd $(closest swops-secret) 35 | 36 | while ! [ -e "$1" ] 37 | do 38 | if [ "$(pwd)" = '/' ] 39 | then 40 | # Didn't find it, blank output 41 | return 1 42 | fi 43 | cd .. 44 | done 45 | printf "%s\n" "$(pwd)/$1" 46 | } 47 | # let $here be the directory that contains the LICENCE file. 48 | here=$(closest LICENCE) 49 | here=$(dirname "$here") 50 | 51 | thisdir=$(basename "$here") 52 | first2=$(printf '%.2s' "$thisdir") 53 | 54 | PS1="[$first2]$PS1" 55 | deactivate () { 56 | PATH=$OLD_PATH 57 | PS1=$OLD_PS1 58 | NODE_PATH=$OLD_NODE_PATH 59 | unset -f deactivate 60 | unset -f mocha 61 | } 62 | 63 | # Source script containing useful, but secret, environment variables. 64 | . ../swops/archive/swops-secret/keys.sh 65 | 66 | export ACK_OPTIONS="--type-add coffee=.coffee${ACK_OPTIONS+ $ACK_OPTIONS}" 67 | alias ack=ack-grep 68 | 69 | [[ -s ${HOME}/.nvm/nvm.sh ]] && . ${HOME}/.nvm/nvm.sh # This loads NVM 70 | # Both mac-users and linux-users love nvm 71 | nvm use 0.10 72 | 73 | true 74 | -------------------------------------------------------------------------------- /migration/2013-08-symlink-migration/generate-migrate-scripts.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | export NODE_ENV=cron 4 | 5 | tee /tmp/migrate.log < /dev/null 6 | 7 | mkfifo /tmp/sw-pipe 8 | trap "wait" EXIT 9 | trap "exec 1>&-" EXIT 10 | trap "rm /tmp/sw-pipe" EXIT 11 | 12 | exec 3>&1 13 | exec 4>&2 14 | 15 | tee -a /tmp/migrate.log < /tmp/sw-pipe >&3 & 16 | exec 1>/tmp/sw-pipe 17 | exec 2>&1 18 | 19 | MIGRATE_FILE="${PWD}/migrate-$(hostname).sh" 20 | 21 | cat < $MIGRATE_FILE 22 | #!/bin/bash 23 | 24 | set -e 25 | 26 | UNMIGRATE_FILE="${PWD}/unmigrate-$(hostname).sh" 27 | : > \$UNMIGRATE_FILE 28 | EOF 29 | i=0 30 | #echo "ffaalqy -- spreadsheet-download " | 31 | util/listGitURLsOfDir /ebs/home | 32 | egrep -v 'new(view|dataset)$' | sort -u | 33 | while read -r BOX GIT_URL TOOLNAME 34 | do 35 | if ./should-migrate.sh $BOX $GIT_URL $TOOLNAME &>> /tmp/migrate.log 36 | then 37 | echo ---- Migrating box $BOX -- $TOOLNAME 38 | export I=$(((i++))) 39 | # 40 | ( 41 | cd "/ebs/home/$BOX/tool" 42 | MHF="$(git status --porcelain --ignored http | egrep '^(!!|\?\?) http/' || true)" 43 | MHF="$(echo -n "$MHF" | cut -d' ' -f2-)" 44 | MIGRATED_HTTP_FILES="$(echo -n "$MHF" | while read -r f; do echo "mkdir -p \""$(dirname "$f")"\"; mv \""tool/$f"\" \""$(dirname "$f")"\" || true"; done)" 45 | #MIGRATED_HTTP_FILES="$(echo -n "$MHF" | while read -r f; do echo "mkdir -p $(dirname $f); mv tool/$f $(dirname $f) || true"; done)" 46 | #MIGRATED_HTTP_FILES="$(for f in $MHF; do echo "mkdir -p $(dirname $f); mv $f $(dirname $f)"; done)" 47 | cat <> $MIGRATE_FILE 48 | 49 | 50 | ################## 51 | # Migrate box $BOX 52 | echo Migrating ${I} $BOX 53 | cd /ebs/home/$BOX 54 | mkdir -p pre-symlink-migration 55 | chown $BOX:databox pre-symlink-migration 56 | if [ -L http ] 57 | then 58 | rm http 59 | fi 60 | mkdir -p http 61 | chown $BOX:databox http 62 | $MIGRATED_HTTP_FILES 63 | mv tool pre-symlink-migration 64 | ln -s /tools/$TOOLNAME tool 65 | chown --no-dereference $BOX:databox tool 66 | cat <> \$UNMIGRATE_FILE 67 | cd /ebs/home/$BOX 68 | rm tool 69 | mv pre-symlink-migration/* . 70 | rmdir pre-symlink-migration 71 | ROLLBACK 72 | EOF 73 | ) 74 | else 75 | echo ---- Skipping box $BOX -- $TOOLNAME 76 | fi 77 | done 78 | -------------------------------------------------------------------------------- /server/code/model/base.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | 3 | # All server models should extend this class. All subclasses 4 | # should ensure that they have defined a dbClass field that 5 | # is the database class to use (when finding). Typically 6 | # this will be something like: 7 | # @dbClass: mongoose.model 'User', userSchema 8 | class ModelBase 9 | constructor: (obj) -> 10 | for k of obj 11 | @[k] = obj[k] 12 | 13 | objectify: -> 14 | # Prepare the object for transmission. Converts it to a 15 | # plain old JavaScript object. Any uninteresting fields 16 | # removed. 17 | res = {} 18 | for k of @ 19 | res[k] = @[k] 20 | delete res.dbInstance 21 | return res 22 | 23 | isNew: () -> 24 | """true when freshly created. false when the object has been saved 25 | or fetched from the database.""" 26 | 27 | return not @id? 28 | 29 | save: (callback) -> 30 | err = if @validate? then @validate() else null 31 | if err? 32 | return callback err 33 | 34 | if not @dbInstance? 35 | @dbInstance = new @constructor.dbClass(@) 36 | @id = @dbInstance._id 37 | @_id = @dbInstance._id #TODO: we should use ONE of these 38 | @createdDate = @dbInstance.createdDate = Date.now() 39 | else 40 | for k of @dbInstance 41 | @dbInstance[k] = @[k] if @hasOwnProperty k 42 | console.log "base.coffee: saving", @constructor.name, @dbInstance 43 | @dbInstance.save callback 44 | 45 | @findAll: (callback) -> 46 | @find {}, callback 47 | 48 | @find: (options, callback) -> 49 | @dbClass.find options, (err, docs) => 50 | if err? 51 | console.warn err 52 | return callback err, null 53 | if docs? 54 | result = for d in docs 55 | @makeModelFromMongo d 56 | callback null, result 57 | else 58 | callback null, null 59 | 60 | @makeModelFromMongo: (mongo_document) -> 61 | # Takes a Mongo document instance and returns an instance of this 62 | # model. 63 | 64 | # Note that this cool "new @" thing creates a fresh instance 65 | # of the same actual class of "this", which will in general 66 | # be some subclass of ModelBase. 67 | newModel = new @ {} 68 | newModel.dbInstance = mongo_document 69 | _.extend newModel, mongo_document.toObject() 70 | return newModel 71 | 72 | module.exports = ModelBase 73 | -------------------------------------------------------------------------------- /bin/find-unsubscribed.coffee: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env coffee 2 | # 3 | # Finds accounts with a premium plan, but not a linked recurly subscription. 4 | 5 | mongoose = require 'mongoose' 6 | async = require 'async' 7 | mongoose.connect process.env.CU_DB 8 | 9 | _ = require 'underscore' 10 | plans = require 'plans.json' 11 | {User} = require 'model/user' 12 | 13 | CLI = false 14 | 15 | main = (TheUser) -> 16 | paying_plans = _.filter(_.keys(plans), (plan) -> 17 | return plans[plan]['$'] 18 | ) 19 | # console.log "paying_plans", paying_plans 20 | 21 | TheUser.find { 'accountLevel': { '$in': paying_plans } }, (err, users) -> 22 | if err? 23 | console.log err 24 | 25 | async.each users, (user, cb) -> 26 | user.getCurrentSubscription (err, subscription) -> 27 | # they're a data services customer 28 | if user.accountLevel == 'dataservices-ec2' 29 | return cb null 30 | # they're paying for their plan 31 | if subscription?.plan?.plan_code == user.accountLevel 32 | return cb null 33 | # scremium is a test account that needs to be on this plan 34 | if user.shortName == 'scremium' 35 | return cb null 36 | # just enable shyam for now, he's mucking with his subscription 37 | if user.shortName == 'owl' 38 | return cb null 39 | 40 | # something is up 41 | console.log "user:", user.shortName, user.displayName, user.email, "mongo plan:", user.accountLevel 42 | if err? 43 | if /You have no Recurly account/.test(err['error']) 44 | console.log " no recurly account" 45 | else 46 | console.log " failed to getCurrentSubscription", err 47 | else if subscription 48 | console.log " subscription:", subscription.plan.plan_code 49 | else 50 | console.log " no subscription - changing to free-trial" 51 | user.accountLevel = 'free-trial' 52 | user.planExpires = undefined 53 | user.save (err, newUser) -> 54 | if err? 55 | console.log " ERROR saving user", err 56 | else 57 | console.log " YEP updated user" 58 | cb null 59 | , (err) -> 60 | process.exit() if CLI? 61 | 62 | if require.main == module 63 | CLI = true 64 | process.env.NODE_ENV = 'cron' 65 | main(User) 66 | 67 | exports.main = main 68 | 69 | -------------------------------------------------------------------------------- /client/template/nav.eco: -------------------------------------------------------------------------------- 1 | 60 | -------------------------------------------------------------------------------- /test/integration/new_dataset.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, base_url, loginAndGo} = require './helper' 4 | 5 | describe 'New dataset tool', -> 6 | 7 | before (done) -> 8 | loginAndGo 'ehg', 'testing', "/datasets", done 9 | 10 | before (done) -> 11 | browser.waitForElementByCss '.dataset-list', 4000, done 12 | 13 | context 'when I click the "new dataset" button', -> 14 | iframeUrl = null 15 | before (done) -> 16 | wd40.click '.new-dataset', -> 17 | browser.waitForElementByCss '#chooser .tool', 4000, done 18 | 19 | context 'when I click on the newdataset tool', -> 20 | before (done) -> 21 | wd40.click '.newdataset.tool', => 22 | browser.waitForElementByTagName 'iframe', 10000, => 23 | browser.url (err, url) => 24 | @currentUrl = url 25 | done err 26 | 27 | before (done) -> 28 | # wait for the tool menu toggle to load 29 | setTimeout done, 1000 30 | 31 | it 'takes me to the dataset settings page', -> 32 | @currentUrl.should.match new RegExp("#{base_url}/dataset/[^/]+/settings") 33 | 34 | it 'and shows that the "Code a dataset" tool is active', (done) -> 35 | wd40.elementByCss '#toolbar .active', (err, link) -> 36 | link.text (err, text) -> 37 | text.toLowerCase().replace('\n',' ').should.include 'code a dataset' 38 | done() 39 | 40 | it 'and shows me the "Code in a dataset" tool contents', (done) -> 41 | wd40.switchToBottomFrame -> 42 | wd40.trueURL (err, url) -> 43 | iframeUrl = url 44 | done() 45 | 46 | context 'when I wait a little while and then go back to the dataset page', -> 47 | before (done) -> 48 | # wait for the data tables tool to be installed in the background 49 | setTimeout done, 5000 50 | 51 | before (done) -> 52 | browser.get @currentUrl.replace(/\/settings$/, ''), done 53 | 54 | it 'shows that the "View in a table" tool is active', (done) -> 55 | wd40.elementByCss '#toolbar .active', (err, link) -> 56 | link.text (err, text) -> 57 | text.toLowerCase().replace('\n',' ').should.include 'view in a table' 58 | done() 59 | 60 | it 'and shows me the "View in a table" tool contents', (done) -> 61 | wd40.switchToBottomFrame -> 62 | wd40.trueURL (err, url) -> 63 | url.should.not.equal iframeUrl 64 | done() 65 | -------------------------------------------------------------------------------- /client/code/view/profile/create-profile.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.CreateProfile extends Backbone.View 2 | className: "create-profile" 3 | 4 | events: 5 | 'click .btn-primary': 'createProfile' 6 | 'keyup #displayname': 'keyupDisplayName' 7 | 'keyup #shortname': 'keyupShortName' 8 | 'blur #shortname': 'keyupDisplayName' 9 | 10 | render: -> 11 | @el.innerHTML = JST['create-profile']() 12 | @ 13 | 14 | keyupShortName: (e) -> 15 | if $(e.target).val() == '' 16 | $(e.target).removeClass('edited') 17 | else 18 | $(e.target).addClass('edited') 19 | 20 | keyupDisplayName: -> 21 | # "is" is a reserved word in coffeescript, so we use 22 | # long form method notation for the .is() jQuery function!! 23 | if not $('#shortname')['is']('.edited') 24 | username = $('#displayname').val() 25 | username = username.toLowerCase().replace(/[^a-zA-Z0-9-.]/g, '') 26 | $('#shortname').val(username) 27 | 28 | createProfile: (e) -> 29 | e.preventDefault() 30 | $button = $(e.target) 31 | 32 | if $.trim($('#shortname').val()) == '' 33 | alert('Sorry. You must supply a shortName.') 34 | return 35 | 36 | $button.attr('disabled', true).addClass('loading').html('Creating Profile…') 37 | 38 | data = 39 | accountLevel: $.trim($('#accountlevel').val()) 40 | displayName: $.trim($('#displayname').val()) 41 | shortName: $.trim($('#shortname').val()) 42 | email: $.trim($('#email').val()) 43 | if $.trim($('#defaultcontext').val()) != '' 44 | data.defaultContext = $.trim($('#defaultcontext').val()) 45 | 46 | $.ajax 47 | url: "#{location.protocol}//#{location.host}/api/user/" 48 | data: data 49 | type: 'POST' 50 | dataType: 'json' 51 | success: (newProfile) => 52 | url = "#{location.protocol}//#{location.host}/set-password/#{newProfile.token}" 53 | @$el.children('form').html("""

New profile “#{newProfile.shortName}” created.

54 |

They can set their password here:

55 |

""") 56 | error: (jqxhr, textStatus, errorThrown) -> 57 | if errorThrown == 'Forbidden' 58 | alert("Hmmm... computer says no. Is your API key a valid staff key?") 59 | else 60 | alert("#{textStatus}: #{errorThrown}: #{jqxhr.responseText}") 61 | $button.attr('disabled', false).removeClass('loading').html(' Try Again') 62 | -------------------------------------------------------------------------------- /test/unit/model/box.coffee: -------------------------------------------------------------------------------- 1 | require '../setup_teardown' 2 | 3 | mongoose = require 'mongoose' 4 | sinon = require 'sinon' 5 | should = require 'should' 6 | _ = require 'underscore' 7 | request = require 'request' 8 | 9 | {Box} = require 'model/box' 10 | 11 | describe 'Box (server)', -> 12 | firstBox = null 13 | before -> 14 | sinon.stub request, 'post', (opts_, cb) -> cb null, statusCode: 200, "{}" 15 | mongoose.connect process.env.CU_DB unless mongoose.connection.db 16 | 17 | after -> 18 | request.post.restore() 19 | 20 | context 'when adding a new box', -> 21 | before (done) -> 22 | Box.create 23 | shortName: 'testofferson' 24 | accountLevel: 'grandfather-ec2' 25 | , (err, box) => 26 | firstBox = box 27 | done err 28 | 29 | it 'assigns a random uid', -> 30 | should.exist firstBox.uid 31 | firstBox.uid.should.be.within 4000, 429496729 32 | 33 | context 'when there is a uid collision', -> 34 | before (done) -> 35 | returnedOneFake = false 36 | 37 | testUid = => 38 | if returnedOneFake 39 | res = 324234324234 40 | else 41 | returnedOneFake = true 42 | res = firstBox.uid 43 | return res 44 | 45 | sinon.stub Box, 'generateUid', testUid 46 | 47 | Box.create 48 | shortName: 'testofferson' 49 | accountLevel: 'grandfather-ec2' 50 | , (err, box) => 51 | @secondBox = box 52 | done() 53 | 54 | after -> 55 | Box.generateUid.restore() 56 | 57 | it "doesn't break, and assigns another random value", -> 58 | should.exist @secondBox.uid 59 | @secondBox.uid.should.not.equal firstBox.uid 60 | 61 | context 'when a box has been given a uid', -> 62 | before (done) -> 63 | firstBox.server = 'bob.scraperwiki.com' 64 | firstBox.save done 65 | 66 | before (done) -> 67 | Box.findOneByName firstBox.name, (err, box) => 68 | @sameBox = box 69 | done() 70 | 71 | it "doesn't change its uid", -> 72 | @sameBox.uid.should.equal firstBox.uid 73 | 74 | context 'when I call Box.listServers', -> 75 | before -> @servers = Box.listServers() 76 | 77 | it 'returns an array of all the box servers', -> 78 | @servers.should.include 'premium.scraperwiki.com' 79 | @servers.should.include 'free-ec2.scraperwiki.com' 80 | @servers.should.include 'ds-ec2.scraperwiki.com' 81 | 82 | it 'gives me four servers (because that is how many there currently are)', -> 83 | @servers.length.should.equal 4 84 | -------------------------------------------------------------------------------- /test/unit/model/handle_error.coffee: -------------------------------------------------------------------------------- 1 | require '../setup_teardown' 2 | 3 | sinon = require 'sinon' 4 | should = require 'should' 5 | helper = require '../helper' 6 | 7 | helper.evalConcatenatedFile 'client/code/util.coffee' 8 | 9 | describe "handleError", -> 10 | context "when we call fetch on a model", -> 11 | beforeEach -> 12 | @myFetch = sinon.stub() 13 | Backbone.Collection.prototype.fetch = @myFetch 14 | Cu.Util.patchErrors() 15 | 16 | MyCollection = Backbone.Collection.extend({ 17 | }) 18 | @myCollection = new MyCollection() 19 | 20 | it "should add a error handler to the options if one isn't present", -> 21 | options = {} 22 | @myCollection.fetch() 23 | 24 | @myFetch.firstCall.args[0].error.should.not.be.null 25 | 26 | it "should not add a error handler to the options if one is present", -> 27 | errorHandler = sinon.stub() 28 | options = {error: errorHandler} 29 | @myCollection.fetch options 30 | 31 | @myFetch.firstCall.args[0].error.should.equal errorHandler 32 | 33 | context "when we call save on a model", -> 34 | beforeEach -> 35 | @mySave = sinon.stub() 36 | Backbone.Model.prototype.save = @mySave 37 | patchErrors() 38 | 39 | MyModel = Backbone.Model.extend({ 40 | }) 41 | @myModel = new MyModel() 42 | 43 | it "should add a error handler to the options if one isn't present", -> 44 | options = {} 45 | @myModel.save {}, options 46 | 47 | @mySave.firstCall.args[1].error.should.not.be.null 48 | 49 | it "should not add a error handler to the options if one is present", -> 50 | errorHandler = sinon.stub() 51 | options = {error: errorHandler} 52 | @myModel.save {}, options 53 | 54 | @mySave.firstCall.args[1].error.should.equal errorHandler 55 | 56 | context "when we call fetch on a model", -> 57 | beforeEach -> 58 | @myFetch = sinon.stub() 59 | Backbone.Model.prototype.fetch = @myFetch 60 | patchErrors() 61 | 62 | MyModel = Backbone.Model.extend({ 63 | }) 64 | @myModel = new MyModel() 65 | 66 | it "should add a error handler to the options if one isn't present", -> 67 | options = {} 68 | @myModel.fetch options 69 | 70 | @myFetch.firstCall.args[0].error.should.not.be.null 71 | 72 | it "should not add a error handler to the options if one is present", -> 73 | errorHandler = sinon.stub() 74 | options = {error: errorHandler} 75 | @myModel.fetch options 76 | 77 | @myFetch.firstCall.args[0].error.should.equal errorHandler 78 | -------------------------------------------------------------------------------- /server/code/lib/email.coffee: -------------------------------------------------------------------------------- 1 | nodemailer = require("nodemailer") 2 | 3 | #TODO: html email (templates?) 4 | 5 | sendGridEmail = (mailOptions, callback) -> 6 | transport = nodemailer.createTransport 'SMTP', 7 | service: "SendGrid" 8 | auth: 9 | user: process.env.CU_SENDGRID_USER 10 | pass: process.env.CU_SENDGRID_PASS 11 | 12 | # send mail with defined transport object 13 | transport.sendMail mailOptions, (err, res) -> 14 | transport.close() 15 | if err? 16 | callback err 17 | else 18 | callback null 19 | 20 | 21 | exports.signUpEmail = (user, token, callback) -> 22 | # `user` should be a user object 23 | # `token` should be a string 24 | mailOptions = 25 | from: 'hello@scraperwiki.com' 26 | to: user.email[0] 27 | subject: "Activate your ScraperWiki account!" 28 | text: """ 29 | Hi #{user.displayName}, 30 | 31 | To activate your ScraperWiki account, please go to: 32 | https://scraperwiki.com/set-password/#{token} 33 | 34 | Thanks, 35 | 36 | ScraperWiki 37 | """ 38 | 39 | sendGridEmail mailOptions, callback 40 | 41 | 42 | exports.passwordResetEmail = (userList, callback) -> 43 | # `userList` should be a list of user objects, with a .token property added. 44 | if userList.length == 1 45 | mailOptions = 46 | from: 'hello@scraperwiki.com' 47 | to: userList[0].email[0] 48 | subject: "Reset your ScraperWiki password" 49 | text: """ 50 | Hi #{userList[0].displayName}, 51 | 52 | Someone has requested a password reset for 53 | your ScraperWiki account. 54 | 55 | If this was you, please reset your password here: 56 | https://scraperwiki.com/set-password/#{userList[0].token} 57 | 58 | Thanks, 59 | 60 | ScraperWiki 61 | """ 62 | else 63 | urlList = [] 64 | for user in userList 65 | urlList.push(""" 66 | Username: #{user.shortName} 67 | 68 | Reset link: https://scraperwiki.com/set-password/#{user.token} 69 | """) 70 | urlList = urlList.join "\n\n" 71 | mailOptions = 72 | from: 'hello@scraperwiki.com' 73 | to: userList[0].email[0] 74 | subject: "Reset your ScraperWiki password" 75 | text: """ 76 | Hi #{userList[0].displayName}, 77 | 78 | Someone has requested a password reset for 79 | your ScraperWiki accounts. 80 | 81 | If this was you, please reset your password here: 82 | 83 | #{urlList} 84 | 85 | Thanks, 86 | 87 | ScraperWiki 88 | """ 89 | 90 | sendGridEmail mailOptions, callback 91 | -------------------------------------------------------------------------------- /test/unit/index.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | 3 | should = require 'should' 4 | request = require 'supertest' 5 | sinon = require 'sinon' 6 | 7 | delete process.env['NODETIME_KEY'] 8 | 9 | index = require '../../server/code/index' 10 | {Dataset} = require '../../server/code/model/dataset' 11 | app = index.app 12 | 13 | 14 | describe 'Express Routes', -> 15 | it '/pricing/', (done) -> 16 | request(app) 17 | .get('/terms/') 18 | .expect(200) 19 | .expect(/Pricing/) 20 | .end(done) 21 | 22 | it '/signup/freetrial/', (done) -> 23 | request(app) 24 | .get('/signup/freetrial/') 25 | .expect(200) 26 | #.expect(/Free Trial/) 27 | .expect(/Create Account/) 28 | .end(done) 29 | 30 | it '/signup/datascientist/', (done) -> 31 | request(app) 32 | .get('/signup/datascientist/') 33 | .expect(200) 34 | #.expect(/Datascientist/) 35 | .expect(/Create Account/) 36 | .end(done) 37 | 38 | it '/signup/exporer/', (done) -> 39 | request(app) 40 | .get('/signup/explorer/') 41 | .expect(200) 42 | #.expect(/Explorer/) 43 | .expect(/Create Account/) 44 | .end(done) 45 | 46 | it '/help/', (done) -> 47 | request(app) 48 | .get('/help/') 49 | .expect(200) 50 | .expect(/Quick start guides/) 51 | .expect(/General help/) 52 | .expect(/Developers/) 53 | .end(done) 54 | 55 | it '/terms/', (done) -> 56 | request(app) 57 | .get('/terms/') 58 | .expect(200) 59 | .expect(/Terms & Conditions/) 60 | .end(done) 61 | 62 | it '/terms/enterprise-agreement/', (done) -> 63 | request(app) 64 | .get('/terms/enterprise-agreement/') 65 | .expect(200) 66 | .expect(/QuickCode Enterprise Agreement/) 67 | .end(done) 68 | 69 | it '/', (done) -> 70 | request(app) 71 | .get('/') 72 | .expect(200) 73 | .expect(/placeholder for the internal home page/) 74 | .end(done) 75 | 76 | it '/login/', (done) -> 77 | request(app) 78 | .get('/login/') 79 | .expect(200) 80 | .expect(/Log in/) 81 | .expect(/Username:/) 82 | .expect(/Password:/) 83 | .end(done) 84 | 85 | describe 'Error Handling', -> 86 | it 'Should display a 404 error', (done) -> 87 | request(app) 88 | .get('/xxsdsdsdsd') 89 | .expect(404) 90 | .expect(/Not found/) 91 | .end(done) 92 | 93 | describe "Status End Point", -> 94 | xit 'Should set the status of the dataset', (done) -> 95 | spy = sinon.spy(Dataset, 'findOneById') 96 | request(app) 97 | .post('/api/status/') 98 | .end (err, res) -> 99 | (spy.calledWith process.env['USER']).should.be.true 100 | done() 101 | 102 | 103 | -------------------------------------------------------------------------------- /test/integration/new_view.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, base_url, loginAndGo} = require './helper' 4 | cleaner = require '../cleaner' 5 | 6 | describe 'New view tool', -> 7 | 8 | before (done) -> 9 | # TODO(pwaller): Not sure why this is needed, but it interacts with the API 10 | # tests otherwise 11 | # NOTE: It hangs trying to click "Apricot", possibly because it's in the non 12 | # tial view. But I'm unsure. 13 | cleaner.clear_and_set_fixtures done 14 | 15 | it "goes to ehg's /datasets", (done) -> 16 | loginAndGo 'ehg', 'testing', "/datasets", done 17 | 18 | context 'when I click on an Apricot dataset', -> 19 | it 'clicks the Apricot dataset', (done) -> 20 | wd40.elementByPartialLinkText 'Apricot', (err, link) -> 21 | link.click done 22 | 23 | it 'takes me to the Apricot dataset page', (done) -> 24 | wd40.trueURL (err, result) -> 25 | result.should.match /\/dataset\/(\w+)/ 26 | done() 27 | 28 | context 'when I click on "More tools" in the toolbar', -> 29 | it 'waits for the window', (done) -> 30 | setTimeout done, 500 # wait for window to get big enough! 31 | 32 | it 'waits for the chooser to appear', (done) -> 33 | wd40.click '#toolbar .new-view', -> 34 | browser.waitForElementByCss '#chooser .tool', 4000, done 35 | 36 | context 'when I click on the newview tool', -> 37 | it 'clicks the new-view tool and waits for it to appear', (done) -> 38 | wd40.click '.newview.tool', => 39 | wd40.waitForInvisibleByCss '#chooser', (err) => 40 | browser.url (err, url) => 41 | @currentUrl = url 42 | done() 43 | , 4000 44 | 45 | it 'waits for the new view page to load', (done) -> 46 | setTimeout done, 2000 47 | 48 | it 'takes me to the view page', (done) -> 49 | wd40.waitForMatchingURL new RegExp("#{base_url}/dataset/[^/]+/view/[^/]+"), done 50 | 51 | context 'when I click on "More tools" in the toolbar (again)', -> 52 | it 'brings up the chooser', (done) -> 53 | wd40.click '#toolbar .new-view', -> 54 | browser.waitForElementByCss '#chooser .tool', 4000, done 55 | 56 | context 'when I click on the newview tool', -> 57 | it 'selects the new view tool, waits for it to disappear', (done) -> 58 | wd40.click '.newview.tool', (err) => 59 | wd40.waitForInvisibleByCss '#chooser', (err) => 60 | browser.url (err, url) => 61 | @currentUrl = url 62 | done() 63 | , 4000 64 | 65 | it 'takes me to the view page', (done) -> 66 | wd40.waitForMatchingURL new RegExp("#{base_url}/dataset/[^/]+/view/[^/]+"), done 67 | 68 | it 'does not show two new view tools', (done) -> 69 | browser.elementsByCssSelector '[data-toolname=newview]', (err, tools) -> 70 | tools.length.should.equal 1 71 | done() 72 | -------------------------------------------------------------------------------- /client/code/app.coffee: -------------------------------------------------------------------------------- 1 | #= require namespace 2 | #= require util 3 | # Must come before any model that uses the mixin 4 | #= require model/boxable 5 | #= require model/tool 6 | #= require model/view 7 | #= require_tree model 8 | #= require_tree router 9 | #= require_tree view 10 | #= require shared/code/page-titles 11 | #= require shared/code/views 12 | 13 | Backbone.View::close = -> 14 | @off() 15 | @remove() 16 | 17 | Backbone.history.routeCount = 0 18 | Backbone.history.on 'route', -> 19 | Backbone.history.routeCount += 1 20 | Backbone.history.firstLoad = false 21 | 22 | Cu.Util.patchErrors() 23 | 24 | if /Trident|MSIE/.test window.navigator.userAgent 25 | $.ajaxSetup 26 | cache: false 27 | 28 | $ -> 29 | window.app = new Cu.Router.Main() 30 | Backbone.history.start {pushState: on, hashChange: false} 31 | 32 | # set a flag whenever the whole page is about to reload or close 33 | # so that we can differentiate aborted ajax requests from failed ones 34 | window.aboutToClose = false 35 | $(window).on 'unload', -> 36 | window.aboutToClose = true 37 | 38 | if Backbone.history and Backbone.history._hasPushState 39 | $(document).delegate "a[href]:not([href^=http], [href^=mailto])", "click", (evt) -> 40 | unless $(@).is '[data-nonpushstate]' 41 | unless evt.metaKey or evt.ctrlKey 42 | href = $(@).attr "href" 43 | evt.preventDefault() 44 | window.app.navigate href, trigger: true 45 | window.scrollTo 0,0 46 | 47 | # :todo: really should extract this information from the API. 48 | window.app.planConvert = 49 | explorer: 'medium-ec2' 50 | datascientist: 'large-ec2' 51 | enterprise: 'xlarge-ec2' 52 | 53 | # Translate from user-visible plan to 54 | # the shortname used for the plan on the subscribe page. 55 | # Only returns a non-null string for paid plans. 56 | window.app.truePlan = (plan) -> 57 | window.app.planConvert[plan] 58 | 59 | # Translate from a plan shortname (like "medium-ec2") 60 | # to a lower-case human readable name. 61 | # Only returns a non-null string for paid plans. 62 | window.app.humanPlan = (plan) -> 63 | _.invert(window.app.planConvert)[plan] 64 | 65 | # Return the user-visible name of the plan, but only when that 66 | # is a paid plan. 67 | window.app.cashPlan = (plan) -> 68 | plan if plan of window.app.planConvert 69 | 70 | 71 | class Cu.AppView 72 | constructor: (@selector) -> 73 | 74 | showView: (view) -> 75 | @currentView?.close() 76 | window.scrollTo 0,0 77 | @currentView = view 78 | @currentView.render() 79 | 80 | $(@selector).show().html @currentView.el 81 | 82 | hideView: (view) -> 83 | @currentView?.close() 84 | $(@selector).hide().empty() 85 | 86 | class Cu.CollectionManager 87 | @collections: {} 88 | 89 | @get: (klass) -> 90 | name = klass.prototype.name 91 | if not @collections[name] 92 | collection = new klass() 93 | collection.fetch 94 | success: -> 95 | collection.trigger 'fetched' 96 | @.collections[name] = collection 97 | return @.collections[name] 98 | -------------------------------------------------------------------------------- /test/integration/helper.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | # Shared before/after functions for all integration tests 3 | 4 | {wd40, browser} = require 'wd40' 5 | cleaner = require '../cleaner' 6 | request = require 'request' 7 | 8 | base_url = process.env.CU_TEST_URL ? 'http://localhost:3001' 9 | login_url = "#{base_url}/login" 10 | logout_url = "#{base_url}/logout" 11 | 12 | prepIntegration = -> 13 | before (done) -> 14 | done(new Error("prepIntegration is deprecated.")) 15 | 16 | loginAndGo = (who, password, url, done) -> 17 | # Login as `who` with `password` if we're not already `who` 18 | # before navigating to `url`. 19 | 20 | target_url = "#{base_url}#{url}" 21 | 22 | browser.eval "window.user", (err, value) -> 23 | return done(err) if err 24 | 25 | doLogin = (cb) -> 26 | browser.get login_url, -> 27 | wd40.fill '#username', who, -> 28 | wd40.fill '#password', password, -> 29 | wd40.click '#login', -> 30 | browser.get target_url, -> 31 | cb() 32 | 33 | if value?.real?.shortName == who 34 | # Already logged in as the right user, go straight there 35 | # This is an optimization to avoid logging in unncessarily. 36 | browser.get target_url, -> 37 | browser.eval "window.location.href", (err, value) -> 38 | return done(err) if err 39 | 40 | if value != target_url 41 | # We didn't make our way to the target. 42 | # This could happen if the database is cleared. 43 | return browser.get logout_url, -> doLogin(done) 44 | done() 45 | return 46 | 47 | # console.log "Logging out because we're the wrong user" 48 | # We're logged in, but the wrong user or not on the scraperwiki site. 49 | # Go via the logout_url. 50 | browser.get logout_url, -> doLogin(done) 51 | 52 | 53 | mediumizeMary = (done) -> 54 | # Ensures medium-mary starts on the right plan 55 | 56 | sub_uuid = '21cc59ce00cb05f2bbc397452a99369c' # medium-mary's subscription 57 | domain = process.env.RECURLY_DOMAIN 58 | pub_key = process.env.RECURLY_API_KEY 59 | 60 | request.put "https://#{domain}.recurly.com/v2/subscriptions/#{sub_uuid}", 61 | auth: 62 | user: pub_key 63 | pass: '' 64 | body: 'medium-ec2' 65 | , done 66 | 67 | enlargeLucy = (done) -> 68 | # Ensures large-lucy starts on the right plan 69 | 70 | sub_uuid = '2131e1fac6fc3d58299a94414bba462e' # large-lucy's subscription 71 | domain = process.env.RECURLY_DOMAIN 72 | pub_key = process.env.RECURLY_API_KEY 73 | 74 | request.put "https://#{domain}.recurly.com/v2/subscriptions/#{sub_uuid}", 75 | auth: 76 | user: pub_key 77 | pass: '' 78 | body: 'large-ec2' 79 | , done 80 | 81 | exports.wd40 = wd40 82 | exports.browser = browser 83 | exports.base_url = base_url 84 | exports.login_url = login_url 85 | exports.logout_url = logout_url 86 | exports.home_url = "#{base_url}/datasets" 87 | exports.prepIntegration = prepIntegration 88 | exports.mediumizeMary = mediumizeMary 89 | exports.enlargeLucy = enlargeLucy 90 | exports.loginAndGo = loginAndGo -------------------------------------------------------------------------------- /server/code/model/tool.coffee: -------------------------------------------------------------------------------- 1 | child_process = require 'child_process' 2 | fs = require 'fs' 3 | exists = fs.exists or path.exists 4 | 5 | async = require 'async' 6 | request = require 'request' 7 | rimraf = require 'rimraf' 8 | 9 | mongoose = require 'mongoose' 10 | Schema = mongoose.Schema 11 | 12 | {Dataset} = require 'model/dataset' 13 | 14 | ModelBase = require 'model/base' 15 | 16 | toolSchema = new Schema 17 | name: 18 | type: String 19 | index: unique: true 20 | user: String 21 | type: String 22 | gitUrl: String 23 | public: {type: Boolean, default: false} 24 | allowedUsers: [String] 25 | allowedPlans: [String] 26 | manifest: Schema.Types.Mixed 27 | created: 28 | type: Date 29 | default: Date.now 30 | updated: Date 31 | 32 | zDbTool = mongoose.model 'Tool', toolSchema 33 | 34 | class exports.Tool extends ModelBase 35 | @dbClass: zDbTool 36 | 37 | loadManifest: (callback) -> 38 | fs.exists @directory, (isok) => 39 | if not isok 40 | callback 'not cloned' 41 | return 42 | fs.readFile "#{@directory}/scraperwiki.json", (err, data) => 43 | if err 44 | callback err 45 | return 46 | try 47 | @manifest = JSON.parse data 48 | catch error 49 | callback error: json: error 50 | callback null 51 | 52 | deleteRepo: (callback) -> 53 | rimraf @directory, callback 54 | 55 | save: (callback) -> 56 | @updated = Date.now() 57 | super callback 58 | 59 | @findOneById: (id, callback) -> 60 | @dbClass.findOne {_id: id}, (err, doc) => 61 | if doc is null 62 | callback err, null 63 | else 64 | callback null, @makeModelFromMongo doc 65 | 66 | @findOneByName: (name, callback) -> 67 | @dbClass.findOne {name: name}, (err, doc) => 68 | if doc is null 69 | callback err, null 70 | else 71 | callback null, @makeModelFromMongo doc 72 | 73 | @findOneForUser: (args, callback) -> 74 | @dbClass.findOne 75 | name: args.name 76 | $or: [ 77 | {user: args.user.shortName} 78 | {public: true} 79 | {allowedUsers: { $in: [args.user.shortName] }} 80 | {allowedPlans: { $in: [args.user.accountLevel] }} 81 | ] 82 | , (err, doc) => 83 | if doc is null 84 | callback err, null 85 | else 86 | callback null, @makeModelFromMongo doc 87 | 88 | @findForUser: (shortName, cb) -> 89 | {User} = require 'model/user' 90 | User.findByShortName shortName, (err, userModel) => 91 | if err 92 | cb err, null 93 | else 94 | @dbClass.find 95 | $or: [ 96 | {user: shortName} 97 | {public: true} 98 | {allowedUsers: { $in: [shortName] }} 99 | {allowedPlans: { $in: [userModel.accountLevel] }} 100 | ] 101 | , (err, docs) => 102 | if docs is null 103 | cb err, null 104 | else 105 | result = (@makeModelFromMongo(doc) for doc in docs) 106 | cb null, result 107 | 108 | exports.dbInject = (dbObj) -> 109 | Tool.dbClass = zDbBox = dbObj 110 | Tool 111 | -------------------------------------------------------------------------------- /shared/vendor/js/jquery.tinysort.min.js: -------------------------------------------------------------------------------- 1 | /*! TinySort 1.5.6 2 | * Copyright (c) 2008-2013 Ron Valstar http://tinysort.sjeiti.com/ 3 | * License: 4 | * MIT: http://www.opensource.org/licenses/mit-license.php 5 | * GPL: http://www.gnu.org/licenses/gpl.html 6 | */ 7 | !function(a,b){"use strict";function c(a){return a&&a.toLowerCase?a.toLowerCase():a}function d(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]==b)return!e;return e}var e=!1,f=null,g=parseFloat,h=Math.min,i=/(-?\d+\.?\d*)$/g,j=/(\d+\.?\d*)$/g,k=[],l=[],m=function(a){return"string"==typeof a},n=function(a,b){for(var c,d=a.length,e=d;e--;)c=d-e-1,b(a[c],c)},o=Array.prototype.indexOf||function(a){var b=this.length,c=Number(arguments[1])||0;for(c=0>c?Math.ceil(c):Math.floor(c),0>c&&(c+=b);b>c;c++)if(c in this&&this[c]===a)return c;return-1};a.tinysort={id:"TinySort",version:"1.5.6",copyright:"Copyright (c) 2008-2013 Ron Valstar",uri:"http://tinysort.sjeiti.com/",licensed:{MIT:"http://www.opensource.org/licenses/mit-license.php",GPL:"http://www.gnu.org/licenses/gpl.html"},plugin:function(){var a=function(a,b){k.push(a),l.push(b)};return a.indexOf=o,a}(),defaults:{order:"asc",attr:f,data:f,useVal:e,place:"start",returns:e,cases:e,forceStrings:e,ignoreDashes:e,sortFunction:f}},a.fn.extend({tinysort:function(){var p,q,r,s,t=this,u=[],v=[],w=[],x=[],y=0,z=[],A=[],B=function(a){n(k,function(b){b.call(b,a)})},C=function(a,b){return"string"==typeof b&&(a.cases||(b=c(b)),b=b.replace(/^\s*(.*?)\s*$/i,"$1")),b},D=function(a,b){var c=0;for(0!==y&&(y=0);0===c&&s>y;){var d=x[y],f=d.oSettings,h=f.ignoreDashes?j:i;if(B(f),f.sortFunction)c=f.sortFunction(a,b);else if("rand"==f.order)c=Math.random()<.5?1:-1;else{var k=e,o=C(f,a.s[y]),p=C(f,b.s[y]);if(!f.forceStrings){var q=m(o)?o&&o.match(h):e,r=m(p)?p&&p.match(h):e;if(q&&r){var t=o.substr(0,o.length-q[0].length),u=p.substr(0,p.length-r[0].length);t==u&&(k=!e,o=g(q[0]),p=g(r[0]))}}c=d.iAsc*(p>o?-1:o>p?1:0)}n(l,function(a){c=a.call(a,k,o,p,c)}),0===c&&y++}return c};for(p=0,r=arguments.length;r>p;p++){var E=arguments[p];m(E)?z.push(E)-1>A.length&&(A.length=z.length-1):A.push(E)>z.length&&(z.length=A.length)}for(z.length>A.length&&(A.length=z.length),s=z.length,0===s&&(s=z.length=1,A.push({})),p=0,r=s;r>p;p++){var F=z[p],G=a.extend({},a.tinysort.defaults,A[p]),H=!(!F||""===F),I=H&&":"===F[0];x.push({sFind:F,oSettings:G,bFind:H,bAttr:!(G.attr===f||""===G.attr),bData:G.data!==f,bFilter:I,$Filter:I?t.filter(F):t,fnSort:G.sortFunction,iAsc:"asc"==G.order?1:-1})}return t.each(function(c,d){var e,f=a(d),g=f.parent().get(0),h=[];for(q=0;s>q;q++){var i=x[q],j=i.bFind?i.bFilter?i.$Filter.filter(d):f.find(i.sFind):f;h.push(i.bData?j.data(i.oSettings.data):i.bAttr?j.attr(i.oSettings.attr):i.oSettings.useVal?j.val():j.text()),e===b&&(e=j)}var k=o.call(w,g);0>k&&(k=w.push(g)-1,v[k]={s:[],n:[]}),e.length>0?v[k].s.push({s:h,e:f,n:c}):v[k].n.push({e:f,n:c})}),n(v,function(a){a.s.sort(D)}),n(v,function(a){var b=a.s,c=a.n,f=b.length,g=c.length,i=f+g,j=[],k=i,l=[0,0];switch(G.place){case"first":n(b,function(a){k=h(k,a.n)});break;case"org":n(b,function(a){j.push(a.n)});break;case"end":k=g;break;default:k=0}for(p=0;i>p;p++){var m=d(j,p)?!e:p>=k&&k+f>p,o=m?0:1,q=(m?b:c)[l[o]].e;q.parent().append(q),(m||!G.returns)&&u.push(q.get(0)),l[o]++}}),t.length=0,Array.prototype.push.apply(t,u),t}}),a.fn.TinySort=a.fn.Tinysort=a.fn.tsort=a.fn.tinysort}(jQuery); -------------------------------------------------------------------------------- /client/code/view/sign-up.coffee: -------------------------------------------------------------------------------- 1 | class Cu.View.SignUp extends Backbone.View 2 | className: 'sign-up' 3 | events: 4 | 'click #go': 'go' 5 | 'keyup #displayName': 'keyupDisplayName' 6 | 'keyup #shortName': 'keyupShortName' 7 | 'blur #shortName': 'keyupDisplayName' 8 | 9 | initialize: (options) -> 10 | @options = options || {} 11 | 12 | render: -> 13 | @el.innerHTML = JST['sign-up']() 14 | 15 | go: (e) -> 16 | e.preventDefault() 17 | $('#go', @$el).addClass('loading').html('Creating Account…') 18 | 19 | @cleanUpForm() 20 | 21 | model = new Cu.Model.User 22 | model.on 'invalid', @displayErrors, @ 23 | 24 | subscribingTo = app.cashPlan @options.plan 25 | model.save 26 | shortName: $('#shortName').val() 27 | email: $('#email').val() 28 | displayName: $('#displayName').val() 29 | subscribingTo: subscribingTo 30 | acceptedTerms: if $('#acceptedTerms').is(':checked') then window.latestTerms else 0 31 | emailMarketing: $('#emailMarketing').is(':checked') 32 | , 33 | success: (model, response, options) => 34 | plan = app.truePlan @options.plan 35 | if plan? 36 | # Note: not logged in, but going to use in the /subscribe page 37 | window.user = effective: model.toJSON() 38 | app.navigate "/subscribe/#{plan}", trigger: true 39 | else 40 | app.navigate "/thankyou", trigger: true 41 | error: (model, response, options) => 42 | $('#go', @$el).removeClass('loading').html('Go!') 43 | 44 | if response.responseText 45 | # Probably an xhr object. 46 | xhr = response 47 | jsonResponse = JSON.parse xhr.responseText 48 | $div = $("""
#{jsonResponse.error or "Something went wrong"}
""") 49 | #TODO: don't prepend, as we end up with multiple alerts 50 | @$el.prepend $div 51 | if jsonResponse.code == 'username-duplicate' 52 | # :todo: Add password reset link. 53 | $div.append(""" Is that you? We have emailed you a password reset link.""") 54 | else 55 | # Don't really know what the error is, so say something technical and geeky. 56 | $div.append("""#{JSON.stringify jsonResponse}""") 57 | 58 | displayErrors: (model_, errors) -> 59 | $('#go', @$el).removeClass('loading').html(' Create Account') 60 | for key of errors 61 | $("##{key}").parents('.controls').append("""#{errors[key]}""").parents('.control-group').addClass('error') 62 | 63 | cleanUpForm: -> 64 | $('.control-group.error', @$el).removeClass('error').find('.help-inline').remove() 65 | 66 | keyupShortName: (e) -> 67 | if $(e.target).val() == '' 68 | $(e.target).removeClass('edited') 69 | else 70 | $(e.target).addClass('edited') 71 | 72 | keyupDisplayName: -> 73 | # "is" is a reserved word in coffeescript, so we use 74 | # long form method notation for the .is() jQuery function!! 75 | if not $('#shortName')['is']('.edited') 76 | username = $('#displayName').val() 77 | username = username.toLowerCase().replace(/[^a-zA-Z0-9-.]/g, '') 78 | $('#shortName').val(username) 79 | -------------------------------------------------------------------------------- /test/integration/create_profile.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, loginAndGo} = require './helper' 4 | 5 | mongoose = require 'mongoose' 6 | {User} = require 'model/user' 7 | 8 | # Overview 9 | # Login as teststaff and use the staff-only /create-profile page 10 | # to create a few new user profiles. 11 | 12 | 13 | before -> 14 | mongoose.connect process.env.CU_DB unless mongoose.connection.db 15 | 16 | checkPasswordLink = (done) -> 17 | wd40.waitForText 'They can set their password here:', -> 18 | wd40.elementByCss '#password-reset-link', (err, element) -> 19 | element.getValue (err, value) -> 20 | value.should.match /^https?:\/\/.+\/set-password\/.+$/ 21 | done() 22 | 23 | describe 'Create a normal user', -> 24 | 25 | before (done) -> 26 | loginAndGo 'teststaff', process.env.CU_TEST_STAFF_PASSWORD, "/create-profile", done 27 | 28 | context 'When I enter new user details', -> 29 | before (done) -> 30 | wd40.click 'option[value="free-trial"]', -> 31 | wd40.fill '#displayname', 'John Smith', -> 32 | wd40.fill '#email', 'john@example.com', done 33 | 34 | it 'it autocompletes the shortName', (done) -> 35 | wd40.elementByCss '#shortname', (err, element) -> 36 | element.getValue (err, value) -> 37 | value.should.equal 'johnsmith' 38 | done() 39 | 40 | context 'When I submit the form', (done) -> 41 | before (done) -> 42 | wd40.click '#create-profile', done 43 | 44 | it 'it gives me a link where the user can set their password', checkPasswordLink 45 | 46 | it 'it has saved the right user details to the database', (done) -> 47 | User.findByShortName 'johnsmith', (err, user) => 48 | should.exist user 49 | user.displayName.should.equal 'John Smith' 50 | user.email.should.eql ['john@example.com'] 51 | done() 52 | 53 | 54 | describe 'Create a user in a corporate datahub', -> 55 | 56 | before (done) -> 57 | loginAndGo 'teststaff', process.env.CU_TEST_STAFF_PASSWORD, "/create-profile", done 58 | 59 | context 'When I enter new user details, and select a default data hub', -> 60 | before (done) -> 61 | wd40.click 'option[value="free-trial"]', -> 62 | wd40.fill '#displayname', 'John Smith the second', -> 63 | wd40.fill '#email', 'john@example.com', -> 64 | wd40.fill '#defaultcontext', 'testersonltd', -> 65 | wd40.click '#create-profile', -> 66 | done() 67 | 68 | it 'it gives me a link where the user can set their password', checkPasswordLink 69 | 70 | it 'it has saved the right user details to the database', (done) -> 71 | User.findByShortName 'johnsmiththesecond', (err, user) => 72 | should.exist user 73 | user.displayName.should.equal 'John Smith the second' 74 | user.email.should.eql ['john@example.com'] 75 | user.should.have.property 'defaultContext' 76 | user.defaultContext.should.equal 'testersonltd' 77 | user.canBeReally.should.be.empty 78 | done() 79 | 80 | it 'it has put the new user into the corporate datahub', (done) -> 81 | User.findByShortName 'testersonltd', (err, company) => 82 | company.canBeReally.should.include 'johnsmiththesecond' 83 | done() 84 | -------------------------------------------------------------------------------- /test/integration/ssh_platform_detect.coffee: -------------------------------------------------------------------------------- 1 | require './setup_teardown' 2 | should = require 'should' 3 | {wd40, browser, loginAndGo} = require './helper' 4 | cleaner = require '../cleaner' 5 | 6 | clickSSHButton = (done) -> 7 | wd40.click '#toolbar a[href$="/settings"] .dropdown-toggle', (err) -> 8 | wd40.click '#tool-options-menu .git-ssh', done 9 | 10 | 11 | # TODO(pwaller): Conditionally disable modal depending on whether we're in 12 | # an environment that supports it. 13 | (if process.env.SKIP_MODAL then xdescribe else describe) 'Platform-specific SSH instructions', -> 14 | 15 | before (done) -> 16 | # Needed so that SSH keys are deleted 17 | cleaner.clear_and_set_fixtures done 18 | 19 | before (done) -> 20 | loginAndGo "ehg", "testing", "/dataset/3006375731", done 21 | 22 | context 'when I use a Windows PC to view SSH instructions', -> 23 | before (done) -> 24 | setTimeout done, 500 25 | 26 | before (done) -> 27 | browser.refresh -> 28 | browser.eval "window.navigator = {platform: 'Win32'}", done 29 | 30 | before clickSSHButton 31 | 32 | before (done) -> 33 | browser.waitForVisibleByCss '.modal', 4000, done 34 | 35 | before (done) => 36 | wd40.getText '.modal', (err, text) => 37 | @modalTextContent = text.toLowerCase() 38 | done() 39 | 40 | it 'the modal window tells me to use Git Bash', => 41 | @modalTextContent.should.include 'git bash' 42 | 43 | it 'the modal window shows me the Windows commands I should run', => 44 | @modalTextContent.should.include 'clip < ~/.ssh/id_rsa.pub' 45 | 46 | context 'when I use a Mac to view SSH instructions', -> 47 | before (done) -> 48 | setTimeout done, 500 49 | 50 | before (done) -> 51 | browser.refresh -> 52 | browser.eval "window.navigator = {platform: 'MacIntel'}", done 53 | 54 | before clickSSHButton 55 | 56 | before (done) -> 57 | browser.waitForVisibleByCss '.modal', 4000, done 58 | 59 | before (done) => 60 | wd40.getText '.modal', (err, text) => 61 | @modalTextContent = text.toLowerCase() 62 | done() 63 | 64 | it 'the modal window tells me to use the Terminal', => 65 | @modalTextContent.should.include 'terminal' 66 | 67 | it 'the modal window shows me the Mac commands I should run', => 68 | @modalTextContent.should.include 'pbcopy < ~/.ssh/id_rsa.pub' 69 | 70 | context 'when I use a Linux computer to view SSH instructions', -> 71 | before (done) -> 72 | setTimeout done, 500 73 | 74 | before (done) -> 75 | browser.refresh -> 76 | browser.eval "window.navigator = {platform: 'Linux i686'}", done 77 | 78 | before clickSSHButton 79 | 80 | before (done) -> 81 | browser.waitForVisibleByCss '.modal', 4000, done 82 | 83 | before (done) => 84 | wd40.getText '.modal', (err, text) => 85 | @modalTextContent = text.toLowerCase() 86 | done() 87 | 88 | it 'the modal window tells me to use the Terminal', => 89 | @modalTextContent.should.include 'terminal' 90 | 91 | it 'the modal window tells me to install xclip', => 92 | @modalTextContent.should.include 'apt-get install xclip' 93 | 94 | it 'the modal window shows me the commands I should run', => 95 | @modalTextContent.should.include 'xclip -sel clip < ~/.ssh/id_rsa.pub' 96 | 97 | --------------------------------------------------------------------------------