diff --git a/.gitignore b/.gitignore index eaad1af3..7767155e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,26 +42,6 @@ doc/ # rubymine stuff /.idea - -# media folder -/media/original/* -/media/cachedimages/* -/media/cachedaudio/* -/media/experiments/* -======= -lib-cov -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz - -pids -logs -results - npm-debug.log node_modules/ build/ diff --git a/.jshintrc b/.jshintrc index d91acc38..24d3f1df 100644 --- a/.jshintrc +++ b/.jshintrc @@ -43,8 +43,8 @@ "boss" : false, // true: Tolerate assignments where comparisons would be expected "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. "eqnull" : true, // true: Tolerate use of `== null` - "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) - "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + "esversion" : 6, // {int} Specify the ECMAScript version to which the code must adhere. + "moz" : true, // true: Allow Mozilla specific syntax (extends and overrides esnext features) // (ex: `for each`, multiple try/catch, function expression…) "evil" : false, // true: Tolerate use of `eval` and `new Function()` "expr" : false, // true: Tolerate `ExpressionStatement` as Programs diff --git a/Gruntfile.js b/Gruntfile.js index d7a4618e..66c1157f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -6,13 +6,13 @@ module.exports = function (grunt) { path = require("path"), slash = require("slash"), _ = require("lodash"), - sass = require("./node_modules/node-sass"); + sass = require("node-sass"); - var _invalidateRequireCacheForFile = function(filePath){ + var _invalidateRequireCacheForFile = function (filePath) { delete require.cache[path.resolve(filePath)]; }; - var requireNoCache = function(filePath){ + var requireNoCache = function (filePath) { _invalidateRequireCacheForFile(filePath); return require(filePath); }; @@ -38,6 +38,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks("grunt-ng-constant"); grunt.loadNpmTasks("grunt-editor"); grunt.loadNpmTasks("grunt-beep"); + grunt.loadNpmTasks("grunt-newer"); /** * Load in our build configuration file. @@ -49,7 +50,8 @@ module.exports = function (grunt) { */ var processVendorJs = require("./buildConfig/vendorTemplateProcessing.js") - (grunt, "./buildConfig/vendor.wrapper", "window.bawApp.externalsCallback", userConfig.vendor_files.jsWrapWithModule); + (grunt, "./buildConfig/vendor.wrapper", "window.bawApp.externalsCallback", + userConfig.vendor_files.jsWrapWithModule); /** @@ -160,8 +162,8 @@ module.exports = function (grunt) { writerOpts: { // conventional-changelog-writer options go here // adapted from https://github.com/ajoslin/conventional-changelog/blob/master/presets/angular.js - transform: function(commit) { - var ignored =["chore", "style", "refactor", "test"]; + transform: function (commit) { + var ignored = ["chore", "style", "refactor", "test"]; if (commit.type === "feat") { commit.type = "Features"; @@ -212,7 +214,7 @@ module.exports = function (grunt) { commit.subject = commit.subject.substring(0, 80); } - _.map(commit.notes, function(note) { + _.map(commit.notes, function (note) { if (note.title === "BREAKING CHANGE") { note.title = "BREAKING CHANGES"; } @@ -239,7 +241,7 @@ module.exports = function (grunt) { "bower.json" ], updateConfigs: [ - "pkg" + "pkg" ], commit: true, commitMessage: "chore(release): v%VERSION%", @@ -305,7 +307,7 @@ module.exports = function (grunt) { "conf.environment": appEnvironment }; - Object.keys(constantsFiles).forEach(function(key) { + Object.keys(constantsFiles).forEach(function (key) { var constantsModule = requireNoCache(constantsFiles[key]); result[key] = constantsModule(appEnvironment); @@ -373,8 +375,7 @@ module.exports = function (grunt) { })() }, build_appjs: { - options: { - }, + options: {}, files: [ { src: ["<%= app_files.js %>", "**/!(*.spec).js.map"], @@ -416,7 +417,9 @@ module.exports = function (grunt) { babel: { options: { sourceMap: true, - optional: ["es7.comprehensions"] + retainLines: true, + + presets: "es2015" }, transpile_appjs: { files: [ @@ -466,7 +469,7 @@ module.exports = function (grunt) { }); }()), "buildConfig/module.prefix", -// "<%= build_dir %>/src/**/*generated.js", + // "<%= build_dir %>/src/**/*generated.js", "<%= build_dir %>/src/**/*.js", "<%= html2js.app.dest %>", "<%= html2js.common.dest %>", @@ -530,8 +533,8 @@ module.exports = function (grunt) { build: { options: { outputStyle: "expanded", - sourceComments: "normal" /*'map', - sourceMap: '<%= sassDestName %>.map'*/ + sourceComments: "normal", + //sourceMap: true // currently broken :-(, refers to the wrong partials }, src: "<%= app_files.processedSass %>", dest: "<%= sassDest %>" @@ -556,15 +559,24 @@ module.exports = function (grunt) { */ jshint: { options: { - jshintrc: ".jshintrc" + jshintrc: ".jshintrc", + reporter: require("jshint-stylish") + }, + src: { + //files: { + src: [ + "<%= app_files.js %>", + "!src/**/*.generated.js" + ] + //} + }, + test: { + // files: { + src: [ + "<%= app_files.jsunit %>" + ] + // } }, - src: [ - "<%= app_files.js %>", - "!src/**/*.generated.js" - ], - test: [ - "<%= app_files.jsunit %>" - ], gruntfile: [ "Gruntfile.js" ] @@ -683,22 +695,26 @@ module.exports = function (grunt) { options: { hostname: "*", port: 8080, - base: "./<%= build_dir %>", - //debug: true, + base: [ + "./" + ], + debug: false, livereload: true, middleware: function (connect, options) { + var buildDirectory = grunt.config("build_dir"); + grunt.log.writeln("Base webserver directory: " + options.base); + grunt.log.writeln("Build webserver directory: " + buildDirectory); - grunt.log.writeln(options.base); return [ modRewrite([ // for source maps - "^/assets/styles/vendor(.*) /vendor$1 [L]", - "^/assets/styles/src(.*) /src$1 [L]", + //"^/assets/styles/vendor(.*) /vendor$1 [L]", + //"^/assets/styles/src(.*) /src$1 [L]", // this rule should match anything under assets and basically not rewrite it - "^/assets(.*) /assets$1 [L]", + //"^/assets(.*) /" + buildDirectory + "/assets$1 [L]", // this rule matches anything without an extension @@ -711,7 +727,13 @@ module.exports = function (grunt) { // with or without a querystring // if matched, the root (index.html) is sent back instead. // from there, angular deals with the route information - "!(\\/[^\\.\\/\\?]+\\.\\w+) / [L]" + //"!(\\/[^\\.\\/\\?]+\\.\\w+) /" + buildDirectory + "/ [L]" + + // does not match any url startng with /build, /src, or /vendor + // if matched, the root (index.html) is sent back instead. + // from there, angular deals with the route information + "!(^(\\/build|\\/src|\\/vendor)) /" + buildDirectory + "/ [L]" + ]), // disable all caching @@ -724,15 +746,29 @@ module.exports = function (grunt) { // will be served from. //connect.static(options.base[0]), gzipStatic(options.base[0]) - - // for source maps - //connect.static(__dirname) ]; } } } }, + /** + * The grunt newer task allows us to only send modified files + * to tasks. This task typically needs no configuration. + * The configuration below is to to do a performance comparison + * (with/without the task). + * The `newer` task send a live reload (after a JS source change) in + * ~5 seconds. With the task disabled it takes about ~40 seconds to + * process all files. + */ + // newer: { + // options: { + // override: function(detail, include) { + // include(true); + // } + // } + // }, + /** * And for rapid development, we have a watch set up that checks to see if * any of the files listed below change, and then to execute the listed @@ -753,7 +789,7 @@ module.exports = function (grunt) { options: { livereload: true, livereloadOnError: false, - spawn: false + //spawn: true }, /** @@ -778,15 +814,15 @@ module.exports = function (grunt) { "<%= app_files.js %>" ], // recent modification: files are copied before unit tests are run! - tasks: ["jshint:src", "beep:error", "ngconstant:build", "babel:transpile_appjs", "copy:build_appjs", "karma:unit:run"] + tasks: ["newer:jshint:src", "beep:error", "ngconstant:build", "newer:babel:transpile_appjs", "copy:build_appjs", "newer:karma:unit:run"] }, jssrc2: { files: [ - "<%= app_files.specialjs %>", + "<%= app_files.specialjs %>" ], // recent modification: files are copied before unit tests are run! - tasks: ["jshint:src", "beep:error","ngconstant:build", "babel:transpile_appjs", "copy:build_appjs", "karma:unit:run"] + tasks: ["jshint:src", "beep:error", "ngconstant:build", "babel:transpile_appjs", "copy:build_appjs", "karma:unit:run"] }, @@ -836,7 +872,7 @@ module.exports = function (grunt) { files: [ "<%= app_files.jsunit %>" ], - tasks: ["babel:transpile_appjs", "jshint:test", "karma:unit:run"], + tasks: ["newer:babel:transpile_appjs", "newer:jshint:test", "karma:unit:run"], options: { livereload: false } @@ -884,7 +920,7 @@ module.exports = function (grunt) { "index:compile" ]); - grunt.registerTask("release", "bump, changelog, commit, and publish to Github", function(type) { + grunt.registerTask("release", "bump, changelog, commit, and publish to Github", function (type) { if (!type) { grunt.fatal(new Error("release task must have a type supplied")); diff --git a/bower.json b/bower.json index 9d289140..a2779263 100644 --- a/bower.json +++ b/bower.json @@ -2,36 +2,41 @@ "name": "baw-client", "version": "0.19.2", "devDependencies": { - "angular": "1.3.x", - "angular-bootstrap": "~0.12.0", + "angular": "1.5.x", + "angular-bootstrap": "1.1.x", "angular-growl-v2": "~0.7.0", - "angular-local-storage": "~0.1.x", - "angular-mocks": "~1.3.x", - "angular-resource": "~1.3.x", - "angular-route": "~1.3.x", - "angular-sanitize": "~1.3.x", + "angular-local-storage": "~0.2.x", + "angular-mocks": "~1.5.x", + "angular-resource": "~1.5.x", + "angular-route": "~1.5.x", + "angular-sanitize": "~1.5.x", "angular-tags": "git://github.com/boneskull/angular-tags.git#master", "angular-ui-utils": "latest", - "bowser": "0.7.x", + "bowser": "1.0.x", "d3": "~3.5.x", - "draggabilly": "~1.1.x", + "draggabilly": "2.x", "hint.css": "https://github.com/chinchang/hint.css.git", "humanize-duration": "^3.4", - "jasmine-expect": "1.x", + "jasmine-expect": "2.x", "jquery-ui": "~1.11.x", "lodash": "4.0", "momentjs": "^2.10", "objectdiff": "https://github.com/NV/objectDiff.js.git", "round-date": "^1.1", - "sass-bootstrap": "3.0.2", - "angular-loading-bar": "~0.7.1", - "ng-form-group": "~1.2.11", - "bootstrap-sass": "3.3-stable" + "angular-loading-bar": "~0.8.x", + "ng-form-group": "^1.3.3", + "bootstrap-sass": "3.3-stable", + "font-awesome": "^4.5.0", + "filesize": "^3.2.1", + "angular-ui-ace": "bower", + "angular-messages": "^1.5.0", + "c3": "git://github.com/masayuki0812/c3.git#98769492d07b6103bfc30a0254ccb1e1ec1cca50" }, "dependencies": {}, "private": true, "resolutions": { - "angular": "1.3.x", - "get-size": "~2.0.2" + "angular": "1.5.x", + "get-size": "~2.0.2", + "font-awesome": "^4.5.0" } } diff --git a/buildConfig/build.config.js b/buildConfig/build.config.js index 1f5dbbd4..4cc91691 100644 --- a/buildConfig/build.config.js +++ b/buildConfig/build.config.js @@ -82,9 +82,11 @@ module.exports = { vendor_files: { jsWrapWithModule: [ "vendor/d3/d3.js", + "vendor/c3/c3.js", "vendor/momentjs/moment.js", "vendor/lodash/lodash.js", "vendor/bowser/bowser.js", + "vendor/filesize/lib/filesize.js", "vendor/humanize-duration/humanize-duration.js", "vendor/round-date/roundDate.js", "node_modules/rbush/rbush.js" @@ -93,6 +95,7 @@ module.exports = { "node_modules/babel-polyfill/dist/polyfill.js", "vendor/jquery/dist/jquery.js", "vendor/angular/angular.js", + "vendor/angular-messages/angular-messages.js", "buildConfig/externalModule.js", @@ -110,6 +113,7 @@ module.exports = { "vendor/momentjs/moment.js", "vendor/humanize-duration/humanize-duration.js", "vendor/round-date/roundDate.js", + "vendor/filesize/lib/filesize.js", "vendor/angular-route/angular-route.js", @@ -127,15 +131,10 @@ module.exports = { "vendor/angular-sanitize/angular-sanitize.js", // draggabilly - "vendor/classie/classie.js", - "vendor/eventEmitter/EventEmitter.js", - "vendor/eventie/eventie.js", - "vendor/get-style-property/get-style-property.js", - // get-size depends on get-style-property... it has to come after it - "vendor/get-size/get-size.js", - "vendor/draggabilly/draggabilly.js", + "vendor/draggabilly/dist/draggabilly.pkgd.js", "vendor/d3/d3.js", + "vendor/c3/c3.js", "node_modules/rbush/rbush.js", "vendor/bowser/bowser.js", @@ -144,7 +143,16 @@ module.exports = { "vendor/angular-local-storage/dist/angular-local-storage.js", - "vendor/angular-loading-bar/build/loading-bar.js" + "vendor/angular-loading-bar/build/loading-bar.js", + + // https://github.com/angular-ui/ui-ace + "vendor/ace-builds/src-min-noconflict/ace.js", + "vendor/ace-builds/src-min-noconflict/ext-whitespace.js", + "vendor/ace-builds/src-min-noconflict/mode-json.js", + "vendor/ace-builds/src-min-noconflict/mode-yaml.js", + "vendor/ace-builds/src-min-noconflict/mode-xml.js", + "vendor/ace-builds/src-min-noconflict/theme-xcode.js", + "vendor/angular-ui-ace/ui-ace.js" ], css: [ // NOTE: bootstrap css imported in application.tpl.scss @@ -157,7 +165,9 @@ module.exports = { "vendor/angular-growl-v2/build/angular-growl.css", - "vendor/angular-loading-bar/build/loading-bar.css" + "vendor/angular-loading-bar/build/loading-bar.css", + + "vendor/c3/c3.css" ], assets: [ // jquery-ui is stoopid, special case @@ -171,6 +181,11 @@ module.exports = { template.src = "vendor/bootstrap-sass/assets/fonts/bootstrap/**"; template.dest += "fonts/bootstrap/"; + return template; + }, + function (template) { + template.src = "vendor/font-awesome/fonts/**"; + template.dest += "fonts/"; return template; } diff --git a/buildConfig/environmentSettings.json b/buildConfig/environmentSettings.json index 3140d23a..7848ddb2 100644 --- a/buildConfig/environmentSettings.json +++ b/buildConfig/environmentSettings.json @@ -2,8 +2,8 @@ "environments" : { "development": { "apiRoot": "https://staging.ecosounds.org", - "siteRoot": "https://localhost:8080", - "siteDir": "/", + "siteRoot": "http://localhost:8080", + "siteDir": "/build/", "ga": { "trackingId": "" } diff --git a/package.json b/package.json index 774802dd..f4d9541a 100644 --- a/package.json +++ b/package.json @@ -13,50 +13,54 @@ "url": "https://github.com/QutBioacoustics/baw-client.git" }, "devDependencies": { - "babel": "^5.5.4", - "babel-polyfill": "^6.9.1", + "babel": "^6.5.2", + "babel-polyfill": "^6.5.0", + "babel-preset-es2015": "^6.5.0", "bower": "^1.4", "connect-gzip-static": "^1.0.0", - "connect-modrewrite": "0.7.x", + "connect-modrewrite": "0.8.x", "dev-ip": "^1.0.1", "grunt": "~0.4.1", - "grunt-babel": "^5.0.1", + "grunt-babel": "6.x", "grunt-beep": "^0.3.2", - "grunt-bump": "^0.3", + "grunt-bump": "^0.7", "grunt-changelog": "^0.3", "grunt-cli": "^0.1.13", - "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-clean": "0.7.x", "grunt-contrib-concat": "^0.5.0", "grunt-contrib-connect": "^0.11", "grunt-contrib-copy": "^0.8", - "grunt-contrib-jshint": "^0.11", - "grunt-contrib-uglify": "^0.9", + "grunt-contrib-jshint": "^0.12", + "grunt-contrib-uglify": "0.11", "grunt-contrib-watch": "~0.6.x", - "grunt-conventional-changelog": "^4.0", + "grunt-conventional-changelog": "^5.0", "grunt-editor": "^0.1.0", "grunt-html2js": "~0.3.2", "grunt-karma": "^0.12", - "grunt-ng-constant": "^1.1.0", + "grunt-newer": "^1.1.1", + "grunt-ng-constant": "2.0", "grunt-sass": "^1.1.0", "jasmine-core": "^2.4.1", + "jshint-stylish": "^2.1.0", "karma": "^0.13.19", "karma-chrome-launcher": "~0.2", - "karma-coverage": "^0.4", + "karma-coverage": "^0.5", "karma-firefox-launcher": "~0.1.3", "karma-jasmine": "^0.3.6", "karma-jasmine-diff-reporter": "^0.3.1", - "karma-phantomjs-launcher": "~0.2", + "karma-phantomjs-launcher": "1.0.x", "karma-sourcemap-loader": "^0.3.5", - "lodash": "^3.7", - "phantomjs": "^1.9", + "lodash": "4.2.x", + "phantomjs-prebuilt": "2.x", "round-date": "^1.1.1", "slash": "^1.0.0" }, "scripts": { "watch": "grunt watch", + "watch-verbose": "grunt watch --verbose", "watch-force": "grunt watch --force", "build": "grunt", - "postinstall": "bower install", + "postinstall": "bower update", "start": "npm run watch", "test": "npm run build" }, diff --git a/src/app/analysisResults/analysisResults.js b/src/app/analysisResults/analysisResults.js new file mode 100644 index 00000000..fb91f33b --- /dev/null +++ b/src/app/analysisResults/analysisResults.js @@ -0,0 +1,4 @@ +angular + .module("bawApp.analysisResults", [ + "bawApp.analysisResults.fileList", + ]); diff --git a/src/app/analysisResults/fileList/fileList.js b/src/app/analysisResults/fileList/fileList.js new file mode 100644 index 00000000..910b13b4 --- /dev/null +++ b/src/app/analysisResults/fileList/fileList.js @@ -0,0 +1,98 @@ +class FileListController { + constructor($scope, $location, $routeParams, $url, growl, AnalysisJobService, AnalysisResultService) { + let controller = this; + + this.analysisJob = null; + this.analysisResult = null; + this.paging = undefined; + this._$location = $location; + this._$url = $url; + + $routeParams.path = $routeParams.path || "/"; + $routeParams.page = Number($routeParams.page); + if (isNaN($routeParams.page)) { + $routeParams.page = undefined; + } + + + // download metadata + AnalysisJobService + .getName(Number($routeParams.analysisJobId)) + .then(function (response) { + controller.analysisJob = response.data.data[0]; + controller.updateCurrentDirectory(); + }) + .then(() => AnalysisResultService.get($routeParams.path, $routeParams.page)) + .then(function (response) { + controller.analysisResult = response.data.data[0]; + + controller.paging = response.data.meta.paging; + if (controller.paging) { + controller.paging.maxPageLinks = 10; + } + + controller.analysisResult.analysisJob = controller.analysisJob; + controller.updateCurrentDirectory(); + }) + .catch((error) => { + console.error("AnalysisJobs::details::error: ", error); + growl.error( + "There was a problem loading this page. Please refresh the page. If you see this message often please let us know."); + }); + + + this.currentDirectory = []; + + } + + updateCurrentDirectory() { + this.currentDirectory = [ + { + path: !this.analysisJob ? "" : this.analysisJob.viewUrl, + title: !this.analysisJob ? "" : (this.analysisJob.name.substring(0, 12) + "…") + }, + { + path: !this.analysisJob ? "" : this.analysisJob.resultsUrl, + title: "results" + } + ]; + + let fragments = []; + + if (this.analysisResult) { + fragments = this + .analysisResult + .path + .split("/") + .filter(s => s !== "") + .map((fragment, i, all) => ({path: this.getPath(fragment, i, all), title: fragment})); + } + + this.currentDirectory = this.currentDirectory.concat(fragments); + + } + + getPath(fragment, i, fragments) { + return this.analysisJob.resultsUrl + "/" + fragments.slice(0, i + 1).join("/"); + } + + getPaginationLink(page) { + return this._$url.formatUri(this._$location.path(), {page}); + } +} + +angular + .module("bawApp.analysisResults.fileList", []) + .controller( + "FileListController", + [ + "$scope", + "$location", + "$routeParams", + "$url", + "growl", + "AnalysisJob", + "AnalysisResult", + FileListController + ]); + diff --git a/src/app/analysisResults/fileList/fileList.tpl.html b/src/app/analysisResults/fileList/fileList.tpl.html new file mode 100644 index 00000000..dca9f77a --- /dev/null +++ b/src/app/analysisResults/fileList/fileList.tpl.html @@ -0,0 +1,103 @@ + +
  • + + + Download this folder + + +
  • +
    +
    +
    +

    Analysis Results +
    + {{ fileList.analysisJob.name }} +

    + + +

    + Results are available as they are generated. There is a basic file explorer below + that can be used for downloading the raw data. +

    +

    + To download an entire directory as a zip file, use the Download this folder + link. +

    +
    +
    +

    Files

    + +
    +
    +
    + +
    + +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + +
    + Name + + Size + + Download +
    + + No files in this folder + +
    + + + {{:: file.name }} + + + + {{:: file.name }} + + {{:: file.friendlySize }} + + + download + +
    + +
    + +
    diff --git a/src/app/annotationLibrary/annotationItem.tpl.html b/src/app/annotationLibrary/annotationItem.tpl.html index ec3836b6..1d7a7cc2 100644 --- a/src/app/annotationLibrary/annotationItem.tpl.html +++ b/src/app/annotationLibrary/annotationItem.tpl.html @@ -1,5 +1,5 @@
    -

    Annotation {{model.audioEventId}}

    +

    Annotation {{ annotation.id }}

    diff --git a/src/app/annotationLibrary/annotationLibrary.tpl.html b/src/app/annotationLibrary/annotationLibrary.tpl.html index ee20841c..a9434dc0 100644 --- a/src/app/annotationLibrary/annotationLibrary.tpl.html +++ b/src/app/annotationLibrary/annotationLibrary.tpl.html @@ -153,7 +153,8 @@

    Annotation Library

    ng-click="searchFilter()">Filter + ng-click="clearFilter()">Clear +
    @@ -165,15 +166,16 @@

    Annotation Library

    Results

    - - + +

      @@ -238,15 +240,16 @@

    - - + +
    \ No newline at end of file diff --git a/src/app/annotationLibrary/common.js b/src/app/annotationLibrary/common.js index e59a868b..b68e559c 100644 --- a/src/app/annotationLibrary/common.js +++ b/src/app/annotationLibrary/common.js @@ -159,7 +159,8 @@ angular // modify annotations by reference annotations.forEach(annotation => { annotationToTagLinker(annotation, { - Tag: tagLookup + Tag: tagLookup, + Tagging: new Map() }); annotation.urls.tagSearch = createFilterUrl({ diff --git a/src/app/annotationLibrary/library.js b/src/app/annotationLibrary/library.js index a85220bb..90742393 100644 --- a/src/app/annotationLibrary/library.js +++ b/src/app/annotationLibrary/library.js @@ -184,7 +184,6 @@ angular {page, items: $scope.paging.items}) ); } - function getPagingSettings(page, items, total) { var paging = { maxPageLinks: 7, diff --git a/src/app/app.js b/src/app/app.js index 1e44f4ae..8aaada74 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -52,10 +52,12 @@ angular.module("baw", "ngRoute", "ngResource", "ngSanitize", + "ngMessages", //'ui.select2', "ui.bootstrap", "ui.bootstrap.typeahead", "ui.bootstrap.tpls", + "ui.ace", // code-editor! used for analysis jobs config "ng-form-group", // connects angular form validation with bootstrap classes "decipher.tags", "angular-growl", @@ -94,11 +96,13 @@ angular.module("baw", "bawApp.accounts", "bawApp.annotationViewer", + "bawApp.analysisResults", "bawApp.audioEvents", "bawApp.annotationLibrary", "bawApp.bookmarks", "bawApp.demo", "bawApp.error", + "bawApp.jobs", "bawApp.home", "bawApp.listen", "bawApp.login", @@ -108,6 +112,8 @@ angular.module("baw", "bawApp.recordInformation", "bawApp.recordings", "bawApp.recordings.recentRecordings", + "bawApp.savedSearches", + "bawApp.scripts", "bawApp.search", "bawApp.tags", "bawApp.users", @@ -119,13 +125,23 @@ angular.module("baw", function ($provide, $routeProvider, $locationProvider, $httpProvider, paths, constants, $sceDelegateProvider, growlProvider, localStorageServiceProvider, cfpLoadingBarProvider, $urlProvider, casingTransformers) { // adjust security whitelist for resource urls var currentWhitelist = $sceDelegateProvider.resourceUrlWhitelist(); - currentWhitelist.push(paths.api.root+"/**"); + currentWhitelist.push(paths.api.root + "/**"); $sceDelegateProvider.resourceUrlWhitelist(currentWhitelist); $routeProvider.whenDefaults = whenDefaults; $routeProvider.fluidIf = baw.fluidIf; + // secondary navs + const analysisJobsNav = { + title: "Analysis Jobs", + href: paths.site.ngRoutes.analysisJobs.list + }; + const analysisJobNav = { + title: "Analysis Job", + href: paths.site.ngRoutes.analysisJobs.details + }; + // routes $routeProvider. when("/home", {templateUrl: "/assets/home.html", controller: "HomeCtrl"}). @@ -139,14 +155,63 @@ angular.module("baw", //whenDefaults("audioEvents", "audioEvent", ":audioEventId", 'AudioEventsCtrl', 'AudioEventCtrl'). //whenDefaults("users", "user", ":userId", 'UsersCtrl', 'UserCtrl'). - when("/recordings", {templateUrl: "/assets/recordings.html", controller: "RecordingsCtrl" }). + when(paths.site.ngRoutes.analysisJobs.list, { + templateUrl: paths.site.files.jobs.list, + controller: "JobsListController", + controllerAs: "jobsList", + title: analysisJobsNav.title, + fullWidth: false, + secondaryNavigation: [], + icon: "tasks" + }). + when(paths.site.ngRoutes.analysisJobs.new, { + templateUrl: paths.site.files.jobs.new, + controller: "JobNewController", + controllerAs: "jobNew", + title: "New Analysis Job", + fullWidth: false, + secondaryNavigation: [ analysisJobsNav ], + icon: "tasks" + }). + when(paths.site.ngRoutes.analysisJobs.details.replace("{analysisJobId}", ":analysisJobId"), { + templateUrl: paths.site.files.jobs.details, + controller: "JobDetailsController", + controllerAs: "jobDetails", + title: analysisJobNav.title, + fullWidth: false, + secondaryNavigation: [ analysisJobsNav ], + icon: "tasks" + }). + when(paths.site.ngRoutes.analysisJobs.analysisResults.replace("{analysisJobId}", ":analysisJobId"), { + templateUrl: paths.site.files.analysisResults.fileList, + controller: "FileListController", + controllerAs: "fileList", + title: "Analysis Job Results", + fullWidth: false, + secondaryNavigation: [ analysisJobsNav, analysisJobNav ], + icon: "table" + }). + //when("/analysis_jobs/:analysisJobsId/edit", {templateUrl: , controller: JobListController, title: "Jobs", + // fullWidth: false}). + + when("/recordings", {templateUrl: "/assets/recordings.html", controller: "RecordingsCtrl"}). when("/recordings/:recordingId", - {templateUrl: "/assets/recording.html", controller: "RecordingCtrl" }). - - when("/listen", {templateUrl: paths.site.files.recordings.recentRecordings, controller: "RecentRecordingsCtrl", title: "Listen"}). - when("/listen/:recordingId", {templateUrl: paths.site.files.listen, controller: "ListenCtrl", title: ":recordingId", fullWidth: true}). - - //when('/listen/:recordingId/start=:start/end=:end', {templateUrl: paths.site.files.listen, controller: 'ListenController'}). + {templateUrl: "/assets/recording.html", controller: "RecordingCtrl"}). + + when("/listen", { + templateUrl: paths.site.files.recordings.recentRecordings, + controller: "RecentRecordingsCtrl", + title: "Listen" + }). + when("/listen/:recordingId", { + templateUrl: paths.site.files.listen, + controller: "ListenCtrl", + title: ":recordingId", + fullWidth: true + }). + + //when('/listen/:recordingId/start=:start/end=:end', {templateUrl: paths.site.files.listen, controller: + // 'ListenController'}). when("/accounts", {templateUrl: "/assets/accounts_sign_in.html", controller: "AccountsCtrl"}). when("/accounts/:action", @@ -154,26 +219,64 @@ angular.module("baw", when("/attribution", {templateUrl: "/assets/attributions.html"}). - when("/birdWalks", {templateUrl: paths.site.files.birdWalk.list, controller: "BirdWalksCtrl", title: "Bird Walks"}). - when("/birdWalks/:birdWalkId", {templateUrl: paths.site.files.birdWalk.detail, controller: "BirdWalkCtrl", title: ":birdWalkId"}). + when("/birdWalks", { + templateUrl: paths.site.files.birdWalk.list, + controller: "BirdWalksCtrl", + title: "Bird Walks" + }). + when("/birdWalks/:birdWalkId", { + templateUrl: paths.site.files.birdWalk.detail, + controller: "BirdWalkCtrl", + title: ":birdWalkId" + }). // experiments when("/experiments/:experiment", {templateUrl: "/assets/experiment_base.html", controller: "ExperimentsCtrl"}). - when("/library", {templateUrl: paths.site.files.library.list, controller: "AnnotationLibraryCtrl", title: "Annotation Library" , fullWidth: true}). + when("/library", { + templateUrl: paths.site.files.library.list, + controller: "AnnotationLibraryCtrl", + title: "Annotation Library", + fullWidth: true + }). when("/library/:recordingId", { - redirectTo: function (routeParams, path, search) { return "/library?audioRecordingId="+routeParams.recordingId;}, + redirectTo: function (routeParams, path, search) { + return "/library?audioRecordingId=" + routeParams.recordingId; + }, templateUrl: paths.site.files.library.list, - title: ":recordingId" , fullWidth: true}). + title: ":recordingId", + fullWidth: true + }). when("/library/:recordingId/audio_events", { - redirectTo: function (routeParams, path, search) { return "/library?audioRecordingId="+routeParams.recordingId;}, - title: "Audio Events" }). - when("/library/:recordingId/audio_events/:audioEventId", {templateUrl: paths.site.files.library.item, controller: "AnnotationItemCtrl", title: "Annotation :audioEventId"}). - - when(paths.site.ngRoutes.demo.d3, {templateUrl: paths.site.files.demo.d3, controller: "D3TestPageCtrl", title: "D3 Test Page" }). - when(paths.site.ngRoutes.demo.rendering, {templateUrl: paths.site.files.demo.rendering, controller: "RenderingCtrl", title: "Rendering" , fullWidth: true }). - when(paths.site.ngRoutes.demo.bdCloud, {templateUrl: paths.site.files.demo.bdCloud2014, controller: "BdCloud2014Ctrl", title: "BDCloud2014 demo" , fullWidth: true }). + redirectTo: function (routeParams, path, search) { + return "/library?audioRecordingId=" + routeParams.recordingId; + }, + title: "Audio Events" + }). + when("/library/:recordingId/audio_events/:audioEventId", { + templateUrl: paths.site.files.library.item, + controller: "AnnotationItemCtrl", + title: "Annotations" + }). + + when(paths.site.ngRoutes.demo.d3, { + templateUrl: paths.site.files.demo.d3, + controller: "D3TestPageCtrl", + title: "D3 Test Page" + }). + when(paths.site.ngRoutes.demo.rendering, { + templateUrl: paths.site.files.demo.rendering, + controller: "RenderingCtrl", + title: "Rendering", + fullWidth: true + }). + when(paths.site.ngRoutes.demo.bdCloud, { + templateUrl: paths.site.files.demo.bdCloud2014, + controller: "BdCloud2014Ctrl", + title: "BDCloud2014 demo", + fullWidth: true + }). when(paths.site.ngRoutes.visualize, { templateUrl: paths.site.files.visualize, @@ -184,9 +287,24 @@ angular.module("baw", }). // missing route page - when("/", {templateUrl: paths.site.files.home, controller: "HomeCtrl"}). - when("/404", {templateUrl: paths.site.files.error404, controller: "ErrorCtrl"}). - when("/404?path=:errorPath", {templateUrl: paths.site.files.error404, controller: "ErrorCtrl"}). + when("/", { + templateUrl: paths.site.files.home, + controller: "HomeCtrl", + title: "Home", + fullWidth: false + }). + when("/404", { + templateUrl: paths.site.files.error404, + controller: "ErrorController", + title: "Not found", + fullWidth: false + }). + when("/404?path=:errorPath", { + templateUrl: paths.site.files.error404, + controller: "ErrorController", + title: "Not found", + fullWidth: false + }). otherwise({ redirectTo: function (params, location, search) { return "/404?path=" + location; @@ -213,10 +331,10 @@ angular.module("baw", localStorageServiceProvider.setPrefix(constants.namespace); // for compatibility with rails api - $urlProvider.registerRenamer("Server", function(key) { + $urlProvider.registerRenamer("Server", function (key) { return casingTransformers.underscore(key); }); - $urlProvider.registerRenamer("Client", function(key) { + $urlProvider.registerRenamer("Client", function (key) { return casingTransformers.camelize(key); }); @@ -234,52 +352,31 @@ angular.module("baw", }]) - .run(["$rootScope", "$location", "$route", "$http", "Authenticator", "AudioEvent", "conf.paths", "UserProfile", "ngAudioEvents", "$url", "predictiveCache", "conf.constants", "predictiveCacheDefaultProfiles", - function ($rootScope, $location, $route, $http, Authenticator, AudioEvent, paths, UserProfile, ngAudioEvents, $url, predictiveCache, constant, predictiveCacheDefaultProfiles) { + .run(["$rootScope", "$location", "$route", "$http", "Authenticator", "AudioEvent", "conf.paths", "UserProfile", "ngAudioEvents", "$url", "predictiveCache", "conf.constants", "conf.environment", "predictiveCacheDefaultProfiles", + function ($rootScope, $location, $route, $http, Authenticator, AudioEvent, paths, UserProfile, ngAudioEvents, $url, predictiveCache, constant, appEnvironment, predictiveCacheDefaultProfiles) { // user profile - update user preferences when they change var eventCallbacks = {}; - eventCallbacks[ngAudioEvents.volumeChanged] = function(event, api, value) { + eventCallbacks[ngAudioEvents.volumeChanged] = function (event, api, value) { if (api.profile.preferences.volume !== value) { api.profile.preferences.volume = value; api.updatePreferences(); } }; - eventCallbacks[ngAudioEvents.muteChanged] = function(event, api, value) { + eventCallbacks[ngAudioEvents.muteChanged] = function (event, api, value) { if (api.profile.preferences.muted !== value) { api.profile.preferences.muted = value; api.updatePreferences(); } }; - eventCallbacks.autoPlay = function(event, api, value) { - if(api.profile.preferences.autoPlay !== value) { + eventCallbacks.autoPlay = function (event, api, value) { + if (api.profile.preferences.autoPlay !== value) { api.profile.preferences.autoPlay = value; api.updatePreferences(); } }; UserProfile.listen(eventCallbacks); - // helper function for printing scope objects - /*baw.exports.print = $rootScope.print = function () { - var seen = []; - var badKeys = ["$digest", "$$watchers", "$$childHead", "$$childTail", "$$listeners", "$$nextSibling", - "$$prevSibling", "$root", "this", "$parent"]; - var str = JSON.stringify(this, - function (key, val) { - if (badKeys.indexOf(key) >= 0) { - return "[Can't do that]"; - } - if (typeof val === "object") { - if (seen.indexOf(val) >= 0) { - return ""; - } - seen.push(val); - } - return val; - }, 4); - return str; - };*/ - // http://www.yearofmoo.com/2012/10/more-angularjs-magic-to-supercharge-your-webapp.html#apply-digest-and-phase $rootScope.$safeApply = function ($scope, fn) { @@ -319,8 +416,11 @@ angular.module("baw", $location.path("/404?path="); }); - //https://docs.angularjs.org/api/ngRoute/service/$route + // https://docs.angularjs.org/api/ngRoute/service/$route $rootScope.$on("$routeChangeSuccess", function (event, current, previous, rejection) { + + let title = $route.current && ( " | " + $route.current.title) || ""; + document.title = appEnvironment.brand.title + title; $rootScope.fullWidth = $route.current.$$route.fullWidth; }); @@ -386,9 +486,9 @@ angular.module("baw", // only do it once - we best not be too annoying var supported = constants.browserSupport; var isSupportedBrowser = false; - var version = parseFloat(bowser.browser.version); + var version = parseFloat(bowser.version); angular.forEach(supported.optimum, function (value, key) { - if (isSupportedBrowser || (bowser.browser[key] && version >= value)) { + if (isSupportedBrowser || (bowser[key] && version >= value)) { isSupportedBrowser = true; } }); @@ -399,11 +499,11 @@ angular.module("baw", var supportedBrowser = false; angular.forEach(supported.supported, function (value, key) { - if (bowser.browser[key]) { + if (bowser[key]) { if (version >= value) { growl.info(supported.baseMessage.format({ - name: bowser.browser.name, - version: bowser.browser.version, + name: bowser.name, + version: bowser.version, reason: "not well tested" })); supportedBrowser = true; @@ -417,8 +517,8 @@ angular.module("baw", if (!supportedBrowser) { growl.warning(supported.baseMessage.format({ - name: bowser.browser.name, - version: bowser.browser.version, + name: bowser.name, + version: bowser.version, reason: "not supported" })); } diff --git a/src/app/d3Bindings/c3/chart.js b/src/app/d3Bindings/c3/chart.js new file mode 100644 index 00000000..2b079ee5 --- /dev/null +++ b/src/app/d3Bindings/c3/chart.js @@ -0,0 +1,88 @@ + +angular.module("bawApp.d3.c3.donut", ["bawApp.vendorServices.auto"]) + .directive("c3Chart", + ["d3", "c3", "moment", function (d3, c3, moment) { + + return { + restrict: "E", + scope: { + data: "<", + width: "<", + height: "<", + options: "<", + oninit: "<" + }, + link: function ($scope, $element, attributes) { + var element = $element[0]; + + var chartElement = d3.select(element); + + let defaultOptions = { + bindto: chartElement, + size: { + width: $scope.width, + height: $scope.height + }, + + oninit: $scope.oninit + }; + + let getSize = () => ({height: $scope.height, width: $scope.width}); + let getOptions = () => Object.assign({}, defaultOptions, $scope.options, {size: getSize()}); + + + var chart; + + // WARNING: expensive watch! + $scope.$watch(() => $scope.data, updateData, true); + // WARNING: expensive watch! + $scope.$watch(() => $scope.options, updateOptions, true); + $scope.$watch(() => $scope.width, updateSize); + $scope.$watch(() => $scope.height, updateSize); + + function initChart() { + if (!$scope.data || (!$scope.data.columns && !$scope.data.rows)) { + return; + } + + let mergedOptions = getOptions(); + mergedOptions.size = getSize(); + mergedOptions.data = $scope.data; + + chart = c3.generate(mergedOptions); + } + + function updateData(newValue) { + if (chart) { + + let options = { + unload: true + }; + Object.assign(options, newValue); + chart.load(options); + } + else { + initChart(); + } + } + + function updateOptions() { + if (chart) { + chart = chart.destroy(); + } + + initChart(); + } + + function updateSize() { + if (chart) { + chart.resize(getSize()); + } + } + + + } + + }; + }] + ); \ No newline at end of file diff --git a/src/app/d3Bindings/d3.js b/src/app/d3Bindings/d3.js index fc79d3ea..fce8450b 100644 --- a/src/app/d3Bindings/d3.js +++ b/src/app/d3Bindings/d3.js @@ -5,5 +5,6 @@ angular.module("bawApp.d3", [ "bawApp.d3.terrainView", "bawApp.d3.timelineView", "bawApp.d3.eventDistribution", - "bawApp.d3.widgets" + "bawApp.d3.widgets", + "bawApp.d3.c3.donut" ]); \ No newline at end of file diff --git a/src/app/d3Bindings/dotView/dotView.js b/src/app/d3Bindings/dotView/dotView.js index 742250c6..d52b58c0 100644 --- a/src/app/d3Bindings/dotView/dotView.js +++ b/src/app/d3Bindings/dotView/dotView.js @@ -35,6 +35,7 @@ angular.module("bawApp.d3.dotView", ["bawApp.vendorServices.auto"]) if (valueItem.year === startYear) { foundYear = true; var foundHour = false; + /* jshint loopfunc:true */ angular.forEach(valueItem.hoursOfDay, (valueHours, keyHours) => { var existingHour = valueHours[0]; if (hour === existingHour) { @@ -156,6 +157,7 @@ angular.module("bawApp.d3.dotView", ["bawApp.vendorServices.auto"]) .attr("r", getRadius) .style("fill", getFill); + /* jshint loopfunc:true */ text .attr("y", j * 20 + 25) .attr("x", (d, i) => xScale(d[0]) - 5) @@ -166,6 +168,7 @@ angular.module("bawApp.d3.dotView", ["bawApp.vendorServices.auto"]) .style("fill", (d) => c(dataIndex)) .style("display", "none"); + /* jshint loopfunc:true */ g.append("text") .attr("y", j * 20 + 25) .attr("x", width + 20) diff --git a/src/app/d3Bindings/eventDistribution/tiles.js b/src/app/d3Bindings/eventDistribution/tiles.js index 8a55d688..7b6af4df 100644 --- a/src/app/d3Bindings/eventDistribution/tiles.js +++ b/src/app/d3Bindings/eventDistribution/tiles.js @@ -355,6 +355,7 @@ angular // delay generation of url // url extremely expensive // beside, only need to generate width/tileSize urls at a time + /* jshint loopfunc:true */ get tileImageUrl() { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#Smart_self-overwriting_lazy_getters delete this.tileImageUrl; diff --git a/src/app/demo/d3TestPage.js b/src/app/demo/d3TestPage.js index 562910bb..92d7bb0a 100644 --- a/src/app/demo/d3TestPage.js +++ b/src/app/demo/d3TestPage.js @@ -34,7 +34,7 @@ angular.module("bawApp.demo.d3", []) // populate selects with options $q - .all([Site.getAllSites(), Project.getAllProjects()]) + .all([Site.getAllSite(), Project.getAllProjectNames()]) .then( function (results) { $scope.sites = results[0].data.data; diff --git a/src/app/error/error.js b/src/app/error/error.js index f5a3b51c..2273e4a4 100644 --- a/src/app/error/error.js +++ b/src/app/error/error.js @@ -1,20 +1,7 @@ -//angular.module('home', []).config(function ($routeProvider, $httpProvider) { -// -// $routeProvider. -// when('/', {templateUrl: '/assets/home.html', controller: HomeCtrl}). -// otherwise({redirectTo: '/'}); -// -// //$httpProvider.defaults.headers. -// // common['X-CSRF-Token'] = $['meta[name=csrf-token]'].attr('content'); -//}); angular.module("bawApp.error", []) - .controller("ErrorCtrl", ["$scope", - - - function ErrorCtrl($scope) { - + .controller("ErrorController", [ + "$scope", + function ErrorController($scope) { $scope.message = "We can't seem to find what you are looking for (404)"; - - } ]); diff --git a/src/app/error/error_404.tpl.html b/src/app/error/error404.tpl.html similarity index 100% rename from src/app/error/error_404.tpl.html rename to src/app/error/error404.tpl.html diff --git a/src/app/jobs/_jobs.scss b/src/app/jobs/_jobs.scss new file mode 100644 index 00000000..27f456a3 --- /dev/null +++ b/src/app/jobs/_jobs.scss @@ -0,0 +1,62 @@ +.analysis-jobs-list { + + .actions .media-list > * { + display: inline-block; + // effectively col-md-2 + width: ($container-md / $grid-columns) * 2 + } + + .list-item-header-a { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + + & > h4 { + flex-grow: 1; + } + & > div { + flex-grow: 1; + } + } +} + +.analysis-job-new, .analysis-job-details { + + .custom-settings-panel { + min-height: 20 * 12px; + width: 100%; + resize: vertical; + + border: 1px solid $input-border; + border-radius: $input-border-radius; + + &[disabled] { + cursor: $cursor-disabled !important; + + background: $list-group-disabled-bg; + + & * { + pointer-events: none; + } + + & .ace_cursor { + display: none; + } + } + + &[readonly] { + background: lighten($input-bg-disabled, 3%); + + & .ace_cursor { + display: none; + } + } + } + + textarea { + resize: vertical; + } + +} diff --git a/src/app/jobs/details/jobDetails.js b/src/app/jobs/details/jobDetails.js new file mode 100644 index 00000000..2ceb7a5a --- /dev/null +++ b/src/app/jobs/details/jobDetails.js @@ -0,0 +1,136 @@ +angular + .module("bawApp.jobs.details", []) + .controller( + "JobDetailsController", [ + "JobsCommon", + "$scope", + "$routeParams", + "$http", + "conf.paths", + "ActiveResource", + "baw.models.associations", + "baw.models.AnalysisJob.progressKeys", + "baw.models.AnalysisJob.statusKeys", + "AnalysisJob", + "Script", + "SavedSearch", + "growl", + "MimeType", + function (JobsCommon, ...dependencies) { + + // this whole jig is necessary to allow us to use extends clause. + // This ugly intersection is the result of using angular DI with + // ES6 classes. + + + class JobDetailsController extends JobsCommon { + constructor( + $scope, $routeParams, $http, paths, ActiveResource, modelAssociations, + keys, statuses, AnalysisJobService, + ScriptService, SavedSearchService, growl, MimeType) { + super(keys, statuses); + let controller = this; + const savedSearchLinker = modelAssociations.generateLinker("AnalysisJob", "SavedSearch"); + const scriptLinker = modelAssociations.generateLinker("AnalysisJob", "Script"); + + this.showResultsRoute = paths.site.ngRoutes.analysisJobs.results; + + AnalysisJobService + .get(Number($routeParams.analysisJobId)) + .then(function (response) { + controller.analysisJob = response.data.data[0]; + ActiveResource.set(controller.analysisJob); + controller.chartData.columns = controller.getData(); + }) + .then(() => { + return ScriptService.get(this.analysisJob.scriptId); + }) + .then((response) => { + let scriptLookup = modelAssociations.arrayToMap(response.data.data); + scriptLinker(this.analysisJob, {Script: scriptLookup}); + + let mode = MimeType.mimeToMode(this.analysisJob.script.executableSettingsMediaType); + controller.aceInstance.setMode("ace/mode/" + mode); + }) + .then(() => { + return SavedSearchService.get(this.analysisJob.scriptId); + }) + .then((response) => { + let savedSearchLookup = modelAssociations.arrayToMap(response.data.data); + savedSearchLinker(this.analysisJob, {SavedSearch: savedSearchLookup}); + }) + .catch((error) => { + console.error("AnalysisJobs::details::error: ", error); + growl.warning( + "There was a problem loading this page. Please refresh the page. If you see this message often please let us know."); + }); + + + this.aceConfig = { + useWrapMode: true, + showGutter: true, + theme: "xcode", + mode: "yaml", + firstLineNumber: 1, + onLoad: function (editor) { + controller.aceInstance = editor.getSession(); + //editor.renderer.$cursorLayer.element.style.display = "none"; + + editor.getSession().setUseSoftTabs(true); + // This is to remove following warning message on console: + // Automatically scrolling cursor into view after selection change this will be + // disabled in the next version set editor.$blockScrolling = Infinity to disable this + // message + editor.$blockScrolling = Infinity; + }, + onChange: controller.aceChanged + }; + + this.chartOptions = { + donut: { + title: "Analysis Job Progress" + }, + legend: { + position: "right" + } + }; + + this.chartWidth = 400; + this.chartHeight = 300; + + + this.chartData = { + colors: this.progressKeyColorMap, + columns: this.getData(), + type: "donut" + + }; + } + + getData() { + if (!this.analysisJob) { + return null; + } + + if (this.analysisJob.isNew || this.analysisJob.isPreparing) { + return [[this.analysisJob.overallStatus, 100]]; + } + else { + let data = []; + Object.keys(this.progressKeyColorMap).forEach((key) => { + if (this.skipProgressKeys.indexOf(key) >= 0) { + return; + } + + data.push([key, this.analysisJob.overallProgress[key] || 0]); + }); + + return data; + } + } + } + + return new JobDetailsController(...dependencies); + } + ]); + diff --git a/src/app/jobs/details/jobDetails.tpl.html b/src/app/jobs/details/jobDetails.tpl.html new file mode 100644 index 00000000..6c73f372 --- /dev/null +++ b/src/app/jobs/details/jobDetails.tpl.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + +
  • + + + Show results + +
  • +
    +
    +
    +

    {{ jobDetails.analysisJob.name }}

    + +

    + {{ jobDetails.analysisJob.description }} + + No description entered. + +

    +

    + Results are available as they are generated. Click + + + Show results + + to see them. +

    +
    +
    + +

    Overview

    + + + +
    +
    +

    Progress

    + +
    +
    +

    Settings used

    + + +

    Data

    + + +

    Analysis

    + + +

    Customised Analysis Settings

    +
    +
    +
    +
    + + +
    +
    \ No newline at end of file diff --git a/src/app/jobs/jobs.js b/src/app/jobs/jobs.js new file mode 100644 index 00000000..c9822ffa --- /dev/null +++ b/src/app/jobs/jobs.js @@ -0,0 +1,7 @@ +angular + .module("bawApp.jobs", [ + "bawApp.jobs.common", + "bawApp.jobs.new", + "bawApp.jobs.details", + "bawApp.jobs.list" + ]); diff --git a/src/app/jobs/jobsCommon.js b/src/app/jobs/jobsCommon.js new file mode 100644 index 00000000..6bb2096f --- /dev/null +++ b/src/app/jobs/jobsCommon.js @@ -0,0 +1,55 @@ +class JobsCommon { // jshint ignore:line + constructor(keys, statuses) { + this.skipProgressKeys = ["total", "preparing"]; + this.progressKeyClassMap = { + [keys.queued]: "warning", + [keys.working]: "info", + [keys.successful]: "success", + [keys.failed]: "danger" + }; + + // .../baw-client/vendor/bootstrap-sass/assets/stylesheets/bootstrap/_variables.scss#18 + this.progressKeyColorMap = { + [keys.queued]: "#f0ad4e", + [keys.working]: "#5bc0de", + [keys.successful]: "#5cb85c", + [keys.failed]: "#d9534f", + ["preparing"]: "#337ab7" + }; + + this.statusKeyClassMap = { + [statuses.new]: "primary", + [statuses.preparing]: "primary", + [statuses.processing]: "info", + [statuses.suspended]: "warning", + [statuses.completed]: "success" + }; + + } + + getType(key) { + return this.progressKeyClassMap[key]; + } + + isProgressKeyVisible(key) { + // if key isn't found in list of keys to skip + // it should be visible + return this.skipProgressKeys.indexOf(key) < 0; + } + + getOverallStatusType(status) { + return this.statusKeyClassMap[status]; + } +} + +angular + .module("bawApp.jobs.common", []) + .factory( + "JobsCommon", + [ + "baw.models.AnalysisJob.progressKeys", + "baw.models.AnalysisJob.statusKeys", + function (...dependencies) { + return JobsCommon; + } + ]); diff --git a/src/app/jobs/list/jobsList.js b/src/app/jobs/list/jobsList.js new file mode 100644 index 00000000..994989df --- /dev/null +++ b/src/app/jobs/list/jobsList.js @@ -0,0 +1,37 @@ +angular + .module("bawApp.jobs.list", []) + .controller( + "JobsListController", + [ + "JobsCommon", + "$scope", + "AnalysisJob", + "conf.paths", + "baw.models.AnalysisJob.progressKeys", + "baw.models.AnalysisJob.statusKeys", + function (JobsCommon, ...dependencies) { + class JobsListController extends JobsCommon{ // jshint ignore:line + constructor($scope, AnalysisJobService, paths, keys, statuses) { + super(keys, statuses); + let controller = this; + + + this.analysisJobs = []; + + this.newAnalysisJobRoute = paths.site.ngRoutes.analysisJobs.new; + + + AnalysisJobService + .query() + .then(function (response) { + controller.analysisJobs = response.data.data; + + }); + } + + } + + return new JobsListController(...dependencies); + + } + ]); diff --git a/src/app/jobs/list/jobsList.tpl.html b/src/app/jobs/list/jobsList.tpl.html new file mode 100644 index 00000000..8ec3a404 --- /dev/null +++ b/src/app/jobs/list/jobsList.tpl.html @@ -0,0 +1,119 @@ + +
  • + + + New analysis job + +
  • +
    +
    +

    Analysis Jobs

    +

    + Analysis jobs can be used to analyze subsets of audio data. +

    +

    + This is a list of analysis jobs you have access to. +

    +

    + Analysis jobs take time to complete. + You can monitor an analysis jobs's progress or see its results by clicking on it below. +

    +

    + +

    + + +
    + +
    +
    +
    + +
    + Audio included: + + + recordings + {{ analysisJob.overallCount }} + , + + + size + {{ analysisJob.friendlySize }} + , + + + duration + {{ analysisJob.friendlyDuration }} + + +
    +
    +
    +
    +
    + Progress + + (last updated {{ analysisJob.friendlyUpdated }}) + +
    + + + {{bar}}% + + + {{ analysisJob.overallStatus }} + + + +
    +
    + +
    +
    +
    + + +
    diff --git a/src/app/jobs/new/jobNew.js b/src/app/jobs/new/jobNew.js new file mode 100644 index 00000000..60ef8e2b --- /dev/null +++ b/src/app/jobs/new/jobNew.js @@ -0,0 +1,142 @@ +const jobNewControllerSymbol = Symbol("JobNewControllerPrivates"); + +class JobNewController { + constructor($scope, $routeParams, $timeout, paths, AnalysisJobService, AnalysisJobModel, ScriptService, MimeType) { + this[jobNewControllerSymbol] = {}; + let privates = this[jobNewControllerSymbol]; + + let controller = this; + + this.jobListPath = paths.site.ngRoutes.analysisJobs.list; + + privates.newSavedSearch = false; + privates.$scope = $scope; + privates.$timeout = $timeout; + privates.$scope.newAnalysisJobForm = null; + + // the new analysis job we are making + this.analysisJob = new AnalysisJobModel(); + + // the available scripts + this.scripts = []; + + // download available scripts + ScriptService + .query() + .then(function (response) { + controller.scripts = response.data.data; + }); + + + this.aceConfig = { + useWrapMode: true, + showGutter: true, + theme: "xcode", + mode: "yaml", + firstLineNumber: 1, + onLoad: (editor) => this.aceLoaded(editor), + onChange: this.aceChanged + }; + + $scope.$watch( + () => this.analysisJob.scriptId, + (newValue) => { + if (newValue === null || newValue === undefined) { + return; + } + + let currentScript = this.scripts.find(x => x.id === newValue); + this.analysisJob.customSettings = currentScript.executableSettings; + this.analysisJob.script = currentScript; + + let mode = MimeType.mimeToMode(currentScript.executableSettingsMediaType); + this[jobNewControllerSymbol].aceInstance.setMode("ace/mode/" + mode); + } + ); + + $scope.$watch( + () => this.analysisJob.selectedSavedSearch, + (newValue) => { + if (newValue === null || newValue === undefined) { + this.analysisJob.savedSearchId = null; + + } + else { + this.analysisJob.savedSearchId = newValue.id; + } + } + ); + } + + aceLoaded(editor) { + this[jobNewControllerSymbol].aceInstance = editor.getSession(); + + this[jobNewControllerSymbol].aceInstance.setUseSoftTabs(true); + + editor.maxLines = Infinity; + + // This is to remove following warning message on console: + // Automatically scrolling cursor into view after selection change this will be disabled in the next + // version set editor.$blockScrolling = Infinity to disable this message + editor.$blockScrolling = Infinity; + } + + aceChanged() { + + } + + get isCreatingNewSavedSearch() { + return this[jobNewControllerSymbol].isCreatingNewSavedSearch; + } + + set isCreatingNewSavedSearch(value) { + let p = this[jobNewControllerSymbol]; + + p.isCreatingNewSavedSearch = value; + + p.newSavedSearch = value; + this.selectedSavedSearch = null; + + // this hack fixes: after form has been submitted, user chooses, new saved search, + // form fields are pristine. + if (p.$scope.newAnalysisJobForm.$invalid && p.$scope.newAnalysisJobForm.$submitted) { + p.$timeout(() => p.$scope.$broadcast("$submitted")); + } + } + + get selectedSavedSearch() { + return this.analysisJob.savedSearch; + } + + set selectedSavedSearch(value) { + this.analysisJob.savedSearch = value; + } + + + scriptSelect(id) { + this.analysisJob.scriptId = id; + } + + submitAnalysisJob() { + console.info("submitAnalysisJob: ", this.analysisJob); + + + } +} + +angular + .module("bawApp.jobs.new", []) + .controller( + "JobNewController", + [ + "$scope", + "$routeParams", + "$timeout", + "conf.paths", + "AnalysisJob", + "baw.models.AnalysisJob", + "Script", + "MimeType", + JobNewController + ]); + diff --git a/src/app/jobs/new/jobNew.tpl.html b/src/app/jobs/new/jobNew.tpl.html new file mode 100644 index 00000000..7d457f56 --- /dev/null +++ b/src/app/jobs/new/jobNew.tpl.html @@ -0,0 +1,238 @@ + + + +
    +
    +

    New Analysis Job

    +

    + Analysis jobs can be used to analyze subsets of audio data. + Use this page to select the data to analyze, choose the analysis to run, + configure the settings for the analysis, and submit it for execution. +

    +

    + Analysis jobs take time to complete. After you create an analysis job + you can monitor its progress from its details page. +

    +
    +
    +
    +

    1. Select data

    +

    What audio data do you want to analyze?

    +

    + Choose a saved search +

    +

    + Creating a saved search... +

    +
    + +
    +
    +

    + Or create a + +

    +
    +
    +
    +

    New saved search

    +
    +
    + +
    +
    +

    + Or + + creating a saved search. +

    +
    +
    + + + A saved search needs to be selected. + +
    +
    +
    +

    2. Select analysis type

    + + + + + + + + + + + + + + + + + + + + + +
    + Available analysis types +
    + + Name + + Version + + Description + + Created by +
    + + + {{ script.name }} + + {{ script.version }} + + {{ script.description }} + + +
    +
    + + + An analysis needs to be selected. + +
    +
    +
    +

    3. Customise analysis

    + +

    + Customise the settings for the analysis below. Don't change them if you're happy with the defaults. +

    + +
    +
    +
    +
    +
    +
    +
    +
    +

    4. Name and description

    + +

    + Give this analysis job a meaningful name and a useful description. +

    + +
    +
    + +
    +

    + +

    +
    + +
    + + A name is required for this job. + The name entered is too short (3 character minimum)> + + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    +

    5. Confirm

    +

    + Double check the settings chosen above. Once the analysis job has started: +

    +
      +
    • The job cannot be stopped
    • +
    • The settings cannot be changed
    • +
    • The results cannot be removed
    • +
    + + + + + + There are errors on the page. Please fix them to continue. + + + +
    + Cancel + +
    + +
    +
    +
    +
    diff --git a/src/app/login/widget/loginWidget.js b/src/app/login/widget/loginWidget.js index c4173556..3383a8ca 100644 --- a/src/app/login/widget/loginWidget.js +++ b/src/app/login/widget/loginWidget.js @@ -21,7 +21,7 @@ angular.module("bawApp.login.loginWidget", []) $scope.profile = UserProfile.profile; }); - $scope.defaultUserImage = paths.site.files.login.defaultImageAbsolute; + $scope.defaultUserImage = paths.site.assets.users.defaultImageAbsolute; $scope.logoutLink = paths.api.links.logoutAbsolute; $scope.loginLink = paths.api.links.loginActualAbsolute; $scope.registerLink = paths.api.links.registerAbsolute; diff --git a/src/app/login/widget/loginWidget.tpl.html b/src/app/login/widget/loginWidget.tpl.html index a50ab7a0..07275bd1 100644 --- a/src/app/login/widget/loginWidget.tpl.html +++ b/src/app/login/widget/loginWidget.tpl.html @@ -4,7 +4,7 @@ ng-href="{{adminLink}}" class="loginWidgetLink" target="_self"> - +
  • diff --git a/src/app/navigation/_navigation.scss b/src/app/navigation/_navigation.scss new file mode 100644 index 00000000..64552948 --- /dev/null +++ b/src/app/navigation/_navigation.scss @@ -0,0 +1,11 @@ +left-nav-bar, right-nav-bar { + ul.nav-pills > li > a { + padding: 5px 10px; + } +} + +right-nav-bar { + a>.fa, a>.glyphicon { + color: $text-color + } +} \ No newline at end of file diff --git a/src/app/navigation/breadcrumbs.js b/src/app/navigation/breadcrumbs.js new file mode 100644 index 00000000..09963eec --- /dev/null +++ b/src/app/navigation/breadcrumbs.js @@ -0,0 +1,30 @@ +class BreadcrumbsController { + constructor($scope, $resource, $route, $routeParams, $location) { + //this.$location = $location; + //this.$route = $route; + } +} + + +angular + .module("bawApp.navigation.breadcrumbs", []) + .controller( + "BreadcrumbsController", + [ + "$scope", + "$resource", + "$route", + "$routeParams", + "$location", + BreadcrumbsController + ]) + .component("breadcrumbs", + { + bindings: { + crumbs: "<" + }, + controller: "BreadcrumbsController", + templateUrl: ["conf.paths", function (paths) { + return paths.site.files.navigation.crumbs; + }] + }); diff --git a/src/app/navigation/leftNavBar.tpl.html b/src/app/navigation/leftNavBar.tpl.html new file mode 100644 index 00000000..a90d0d6f --- /dev/null +++ b/src/app/navigation/leftNavBar.tpl.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/src/app/navigation/navigation.js b/src/app/navigation/navigation.js index 59175d5a..28e2ca21 100644 --- a/src/app/navigation/navigation.js +++ b/src/app/navigation/navigation.js @@ -1,19 +1,7 @@ -angular.module("bawApp.navigation", []) - - .directive("navigation", ["conf.paths", function (paths) { - - return { - restrict: "E", - templateUrl: paths.site.files.navigation - }; - }]) - - .controller( - "NavigationCtrl", - ["$scope", "$resource", "$route", "$routeParams", "$location", "breadcrumbs", - function NavigationCtrl($scope, $resource, $route, $routeParams, $location, breadcrumbs) { - $scope.$location = $location; - $scope.$route = $route; - $scope.breadcrumbs = breadcrumbs; - } - ]); \ No newline at end of file +angular.module( + "bawApp.navigation", + [ + "bawApp.navigation.breadcrumbs", + "bawApp.navigation.secondaryNavigation" + ] +); diff --git a/src/app/navigation/navigation.tpl.html b/src/app/navigation/navigation.tpl.html index e98d9a4d..cb29bdb4 100644 --- a/src/app/navigation/navigation.tpl.html +++ b/src/app/navigation/navigation.tpl.html @@ -1,7 +1,12 @@ - + diff --git a/src/app/navigation/rightNavBar.tpl.html b/src/app/navigation/rightNavBar.tpl.html new file mode 100644 index 00000000..250c0507 --- /dev/null +++ b/src/app/navigation/rightNavBar.tpl.html @@ -0,0 +1,25 @@ + diff --git a/src/app/navigation/secondaryNavigation.js b/src/app/navigation/secondaryNavigation.js new file mode 100644 index 00000000..aa4f1c82 --- /dev/null +++ b/src/app/navigation/secondaryNavigation.js @@ -0,0 +1,164 @@ +angular + .module("bawApp.navigation.secondaryNavigation", []) + .factory("ActiveResource", function () { + var activeResource = { + resource: null, + set(newValue) { + this.resource = newValue; + }, + get() { + return this.resource; + } + }; + + return activeResource; + }) + .controller("SecondaryNavigationController", [ + "$rootScope", "$location", "$route", "conf.paths", "ActiveResource", + function ($rootScope, $location, $route, paths, ActiveResource) { + var controller = this; + + const omnipresentLinks = [ + { + title: "Home", + href: paths.api.links.homeAbsolute + }, + { + title: "Projects", + href: paths.api.links.projectsAbsolute + } + ]; + + this.title = ""; + this.links = omnipresentLinks; + this.activeResource = null; + this.icon = null; + this.actionItemsTemplate = null; + + $rootScope.$on("$routeChangeSuccess", onRouteChangeSuccess); + $rootScope.$watch( + () => ActiveResource.resource, + () => controller.activeResource = ActiveResource.get() + ); + + function onRouteChangeSuccess(event, current, previous, rejection) { + // reset the active resource + ActiveResource.set(null); + + controller.title = current.$$route.title; + controller.icon = current.$$route.icon; + controller.actionItemsTemplate = current.$$route.actionsTemplateUrl; + + let currentLink = {title: controller.title, href: $location.$$path}; + let extraLinks = current.$$route.secondaryNavigation || []; + + controller.links = omnipresentLinks + .concat(extraLinks) + .concat(currentLink) + .map(activePath.bind(null, current.$$route)); + } + + let activePath = function (route, link) { + link.isActive = route.regexp.test(link.href); + return link; + }; + }]) + .component("leftNavBar", { + controller: "SecondaryNavigationController", + templateUrl: ["conf.paths", function (paths) { + return paths.site.files.navigation.left; + }] + }) + .component("rightNavBar", { + controller: "SecondaryNavigationController", + templateUrl: ["conf.paths", function (paths) { + return paths.site.files.navigation.right; + }] + }) + .directive("layout", [ + "$cacheFactory", + "$timeout", + "$rootScope", + function ($cacheFactory, $timeout, $rootScope) { + const layoutCacheKey = "layoutCache", + maxRenderAttempts = 2; + + var layoutCache = $cacheFactory(layoutCacheKey), + renderers = []; + + function storeLayout(layoutKey, linkArguments) { + // remove the original layout directive + linkArguments.element[0].remove(); + + layoutCache.put(layoutKey, linkArguments); + } + + function applyLayout(renderer, renderAttempts = maxRenderAttempts) { + //console.debug("layoutDirective::link::applyLayout: ", renderer.layoutKey); + + let storedLayout = layoutCache.get(renderer.layoutKey); + + if (storedLayout === undefined) { + if (renderAttempts <= 0) { + console.warn(`layout rendering failed for layoutKey '${renderer.layoutKey}' after ${maxRenderAttempts} attempts`); + return; + } + + // try again next render loop! + //console.debug(`The \`layout\` directive has no stored content for the \`render-for\` key '${renderer.layoutKey}' - trying again`); + renderAttempts--; + $timeout(applyLayout, 0, true, renderer, renderAttempts); + return; + } + + storedLayout.transclude(function(clone, scope) { + renderer.element.append(clone); + renderer.source = {content: clone, scope}; + }); + } + + function onRouteChangeStart(event, current, previous, rejection) { + renderers.forEach((renderer) => { + if (renderer.source) { + renderer.source.content.remove(); + renderer.source.scope.$destroy(); + renderer.source = null; + } + }); + + layoutCache.removeAll(); + } + + function viewContentLoaded() { + for (let renderer of renderers) { + applyLayout(renderer); + } + } + + $rootScope.$on("$routeChangeStart", onRouteChangeStart); + $rootScope.$on("$viewContentLoaded", viewContentLoaded); + + return { + transclude: true, + restrict: "EA", + scope: { + contentFor: "@", + renderFor: "@" + }, + link: function (scope, element, attr, ctrl, transclude) { + if (attr.contentFor && attr.renderFor) { + throw "The `layout` directive cannot have `content-for` and `render-for` attributes set at the same time."; + } + + if (attr.contentFor) { + //console.debug("layoutDirective::link::contentFor: ", attr.contentFor); + storeLayout(attr.contentFor, {scope, element, attr, ctrl, transclude}); + } + + if (attr.renderFor) { + //console.debug("layoutDirective::link::renderFor: ", attr.renderFor); + renderers.push({layoutKey: attr.renderFor, scope, element, attr, ctrl, transclude}); + } + } + }; + }]); diff --git a/src/app/savedSearches/savedSearches.js b/src/app/savedSearches/savedSearches.js new file mode 100644 index 00000000..0e396396 --- /dev/null +++ b/src/app/savedSearches/savedSearches.js @@ -0,0 +1,7 @@ +angular + .module("bawApp.savedSearches", [ + "bawApp.savedSearches.widgets.new", + "bawApp.savedSearches.widgets.list", + "bawApp.savedSearches.widgets.show" + ]); + diff --git a/src/app/savedSearches/widgets/listSavedSearches.js b/src/app/savedSearches/widgets/listSavedSearches.js new file mode 100644 index 00000000..9db0ce5c --- /dev/null +++ b/src/app/savedSearches/widgets/listSavedSearches.js @@ -0,0 +1,37 @@ +class ListSavedSearchesController { + constructor(SavedSearchService) { + let controller = this; + this.savedSearches = null; + + //this.selectedSavedSearch = null; + + // download saved searches + SavedSearchService + .query() + .then(function (response) { + controller.savedSearches = response.data.data; + }); + } + + get savedSearchesExist() { + return this.savedSearches !== null && this.savedSearches.length !== 0; + } +} + +angular + .module("bawApp.savedSearches.widgets.list", []) + .controller("ListSavedSearchesController", + [ + "SavedSearch", + ListSavedSearchesController + ]) + .component("listSavedSearches", { + bindings: { + selectedSavedSearch: "=selected" + }, + controller: "ListSavedSearchesController", + templateUrl: ["conf.paths", function (paths) { + return paths.site.files.savedSearches.list; + }] + }); + diff --git a/src/app/savedSearches/widgets/listSavedSearches.tpl.html b/src/app/savedSearches/widgets/listSavedSearches.tpl.html new file mode 100644 index 00000000..d50db2f2 --- /dev/null +++ b/src/app/savedSearches/widgets/listSavedSearches.tpl.html @@ -0,0 +1,48 @@ + + +
    +

    No saved searches exist. Try creating a new one.

    +
    + + + + + + + + + + + + + + + + + + +
    + Available saved searches +
    + + Name + + Description + + Created by +
    + + + {{ savedSearch.name }} + + {{ savedSearch.description }} + + +
    \ No newline at end of file diff --git a/src/app/savedSearches/widgets/newSavedSearch.js b/src/app/savedSearches/widgets/newSavedSearch.js new file mode 100644 index 00000000..7e5483e6 --- /dev/null +++ b/src/app/savedSearches/widgets/newSavedSearch.js @@ -0,0 +1,95 @@ +const newSavedSearchControllerSymbol = Symbol("newSavedSearchControllerPrivates"); + +class NewSavedSearchController { + constructor($scope, SavedSearchModel, ProjectService, SiteService) { + //let controller = this; + this[newSavedSearchControllerSymbol] = {}; + let privates = this[newSavedSearchControllerSymbol]; + privates.SiteService = SiteService; + privates.$scope = $scope; + + // when created make sure there is a model to edit + if (!this.newSavedSearch || !(this.newSavedSearch instanceof SavedSearchModel)) { + this.newSavedSearch = new SavedSearchModel(); + } + + // storage for dynamic bits + this.projects = []; + this.sites = []; + + // settings + + this.dateSettingsStart = { + maxDate: this.newSavedSearch.basicFilter.maximumDate + }; + + this.dateSettingsEnd = { + minDate: this.newSavedSearch.basicFilter.minimumDate + + }; + + + // load projects + ProjectService + .getAllProjectNames() + .then((response) => this.projects = response.data.data); + + + // WARNING: object structural equality watcher! + $scope.$watch( + () => this.newSavedSearch, + () => { + this.newSavedSearch.updateQueryFromBasicFilter(); + this.dateSettingsStart.maxDate = this.newSavedSearch.basicFilter.maximumDate; + this.dateSettingsEnd.minDate = this.newSavedSearch.basicFilter.minimumDate; + }, + true + ); + + $scope.$watch( + () => this.newSavedSearch.basicFilter.projectId, + (newValue) => { + if (newValue) { + this.loadSites(); + } + } + ); + } + + selectAllSites(siteSelectorModel) { + this.newSavedSearch.basicFilter.siteIds = this.sites.map(s => s.id); + siteSelectorModel.$setDirty(); + } + + loadSites() { + this[newSavedSearchControllerSymbol].SiteService + .getSitesByProjectIds([this.newSavedSearch.basicFilter.projectId]) + .then((response) => this.sites = response.data.data); + } + + suggestName() { + this.newSavedSearch.name = this.newSavedSearch.generateSuggestedName(this.projects, this.sites); + } + +} + +angular + .module("bawApp.savedSearches.widgets.new", []) + .controller("NewSavedSearchController", [ + "$scope", + "baw.models.SavedSearch", + "Project", + "Site", + NewSavedSearchController + ]) + .component("newSavedSearch", { + bindings: { + newSavedSearch: "=model", + }, + controller: "NewSavedSearchController", + templateUrl: [ + "conf.paths", function (paths) { + return paths.site.files.savedSearches.new; + }] + }); + diff --git a/src/app/savedSearches/widgets/newSavedSearch.tpl.html b/src/app/savedSearches/widgets/newSavedSearch.tpl.html new file mode 100644 index 00000000..e33c7d2b --- /dev/null +++ b/src/app/savedSearches/widgets/newSavedSearch.tpl.html @@ -0,0 +1,165 @@ + + +

    Which audio data?

    +
    + +
    + + + A project must be selected for this job. Please choose one from the list. + + +
    +
    +
    + +
    +

    + +

    +

    + +

    + + Sites are required to be selected. You can select all sites with the button + above. + + +
    +
    + +

    Which dates?

    +
    + +
    +
    + + + + +
    + + The date entered is in an invalid format. Must match the pattern YYYY-MM-DD. + + The start date must be less than the end date. + +
    +
    + +
    + +
    +
    + + + + +
    + + The date entered is in an invalid format. Must match the pattern YYYY-MM-DD. + + The end date must be greater than the start date. + +
    +
    + +

    Name and description

    +
    + +
    +

    + +

    +
    + +
    + + A name is required for this saved search. + The name entered is too short (3 character minimum)> + +
    +
    + +
    + +
    + +
    +
    + +
    diff --git a/src/app/savedSearches/widgets/showSavedSearch.js b/src/app/savedSearches/widgets/showSavedSearch.js new file mode 100644 index 00000000..56e4282d --- /dev/null +++ b/src/app/savedSearches/widgets/showSavedSearch.js @@ -0,0 +1,24 @@ +class ShowSavedSearchController { + constructor(SavedSearchService) { + //let controller = this; + + } +} + +angular + .module("bawApp.savedSearches.widgets.show", []) + .controller("ShowSavedSearchController", + [ + "SavedSearch", + ShowSavedSearchController + ]) + .component("showSavedSearch", { + bindings: { + savedSearch: "=savedSearch" + }, + controller: "ShowSavedSearchController", + templateUrl: ["conf.paths", function (paths) { + return paths.site.files.savedSearches.show; + }] + }); + diff --git a/src/app/savedSearches/widgets/showSavedSearch.tpl.html b/src/app/savedSearches/widgets/showSavedSearch.tpl.html new file mode 100644 index 00000000..554a3deb --- /dev/null +++ b/src/app/savedSearches/widgets/showSavedSearch.tpl.html @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/src/app/scripts/scripts.js b/src/app/scripts/scripts.js new file mode 100644 index 00000000..f195e8a4 --- /dev/null +++ b/src/app/scripts/scripts.js @@ -0,0 +1,5 @@ +angular + .module("bawApp.scripts", [ + "bawApp.scripts.widgets.show" + ]); + diff --git a/src/app/scripts/widgets/showScript.js b/src/app/scripts/widgets/showScript.js new file mode 100644 index 00000000..0cbdde37 --- /dev/null +++ b/src/app/scripts/widgets/showScript.js @@ -0,0 +1,12 @@ + +angular + .module("bawApp.scripts.widgets.show", []) + .component("showScript", { + bindings: { + script: "=bawScript" + }, + templateUrl: ["conf.paths", function (paths) { + return paths.site.files.scripts.show; + }] + }); + diff --git a/src/app/scripts/widgets/showScript.tpl.html b/src/app/scripts/widgets/showScript.tpl.html new file mode 100644 index 00000000..a58ecea5 --- /dev/null +++ b/src/app/scripts/widgets/showScript.tpl.html @@ -0,0 +1,25 @@ +
    +
    +
    +
    +

    + {{ $ctrl.script.name }} +

    +

    + {{ $ctrl.script.description }} +

    +

    + (version {{ $ctrl.script.version }}) +

    +
    +
    + + Created by + +
    +
    +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/app/search/_saved_searches.scss b/src/app/search/_saved_searches.scss deleted file mode 100644 index ad5863ee..00000000 --- a/src/app/search/_saved_searches.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the SavedSearches controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/src/app/search/search.js b/src/app/search/search.js index 771a61c9..99fa5151 100644 --- a/src/app/search/search.js +++ b/src/app/search/search.js @@ -1,35 +1,5 @@ -angular.module("bawApp.search", []) +angular + .module("bawApp.search", [ -.controller("SearchesCtrl", ["$scope", "$resource", "Search", -function SearchesCtrl($scope, $resource, Search) { -// $scope.sitesResource = $resource('/sites', {}, { get: { method:'GET', params:{}, isArray: true }}); -// $scope.sites = $scope.sitesResource.get(); -}]) +]); -.controller("SearchCtrl", ["$scope", "$resource", "Search", - -function SearchCtrl($scope, $resource, Search) { -// $scope.sitesResource = $resource('/sites', {}, { get: { method:'GET', params:{}, isArray: true }}); -// $scope.sites = $scope.sitesResource.get(); - - $scope.projects = [ {name: "demo", id: 6}, {name: "dddemo", id: 7}, {name: "ddaademo", id: 1}, {name: "desssdmo", id: 12}]; - $scope.selectedProjects = [$scope.projects[0].id]; - - $scope.sites = [ {name: "fffff", id: 425}, {name: "ddddd", id: 587}, {name: "ssss", id: 374}, {name: "aaaaa", id: 175}]; - $scope.selectedSites= [$scope.sites[0].id]; - - // $scope.tags = ... - $scope.selectedTags =[]; - - $scope.jobAnnotations = "Include"; - $scope.referenceAnnotations = "Include"; - - $scope.startDate = undefined; - $scope.endDate = undefined; - - // $scope.tags = ... - // $scope.audioRecordings = ... - $scope.selectedAudioRecordings =[]; - - -}]); \ No newline at end of file diff --git a/src/app/users/_userTile.scss b/src/app/users/_userTile.scss new file mode 100644 index 00000000..074a9275 --- /dev/null +++ b/src/app/users/_userTile.scss @@ -0,0 +1,5 @@ +user-tile { + .skinny { + margin: 0; + } +} diff --git a/src/app/users/userTile.js b/src/app/users/userTile.js new file mode 100644 index 00000000..4154c8cf --- /dev/null +++ b/src/app/users/userTile.js @@ -0,0 +1,54 @@ +angular + .module("bawApp.users.userTile", []) + .controller("UserTileController", [ + "$scope", "moment", "conf.paths", "UserProfile", + function ($scope, moment, paths, UserProfile) { + var self = this; + let userKey, dateKey; + + this.defaultUserImage = paths.site.assets.users.defaultImageAbsolute; + this.userProfile = null; + this.friendlyDate = null; + + $scope.$watch( + (scope) => scope.$ctrl.resource, + function () { + if (!self.resource) { + return; + } + + // update the user profile + UserProfile + .getUserForMetadataTile(self.resource[userKey]) + .then((result) => { + self.userProfile = result.data.data[0]; + }); + + // update the friendly date + self.friendlyDate = moment(self.resource[dateKey]).fromNow(); + }); + + this.$onInit = function () { + let created = this.mode === "created", + modified = this.mode === "modified"; + + if (!!(created ^ modified) === false) { // jshint ignore:line + throw new Error("The `mode` attribute must be set to `created` or `modified`"); + } + + userKey = created ? "creatorId" : "updaterId"; + dateKey = created ? "createdAt" : "updatedAt"; + }; + }]) + .component("userTile", { + bindings: { + resource: "<", + mode: "@", + skinny: "@" + }, + controller: "UserTileController", + templateUrl: ["conf.paths", function (paths) { + return paths.site.files.users.userTile; + }], + transclude: true + }); diff --git a/src/app/users/userTile.tpl.html b/src/app/users/userTile.tpl.html new file mode 100644 index 00000000..e5a75421 --- /dev/null +++ b/src/app/users/userTile.tpl.html @@ -0,0 +1,17 @@ + +
    +
    + + {{ $ctrl.userProfile.userName }} + +
    +
    + + {{ $ctrl.userProfile.userName }} + +
    + {{ $ctrl.friendlyDate }} +
    +
    diff --git a/src/app/users/users.js b/src/app/users/users.js index aad05b2b..8c7563cd 100644 --- a/src/app/users/users.js +++ b/src/app/users/users.js @@ -1,8 +1,10 @@ -angular.module("bawApp.users", []) - - .controller("UsersCtrl", ["$scope", "$location", "$resource", "$routeParams", "moment", "User", - - +angular + .module("bawApp.users", [ + "bawApp.users.userTile" + ]).controller( + "UsersCtrl", + [ + "$scope", "$location", "$resource", "$routeParams", "moment", "User", function UsersCtrl($scope, $location, $resource, $routeParams, moment, User) { var userResource = User; var routeArgs = {userId: $routeParams.userId}; diff --git a/src/app/visualize/_visualize.scss b/src/app/visualize/_visualize.scss index 2e03165f..797307fb 100644 --- a/src/app/visualize/_visualize.scss +++ b/src/app/visualize/_visualize.scss @@ -42,17 +42,19 @@ } } - .temporalContextLabel { - font-size: 8pt; - font-weight: normal; - } - .loadingImage { - $loadGifPath: image-url('load.gif'); - background: $loadGifPath center center no-repeat; + //$loadGifPath: image-url('load.gif'); + //background: $loadGifPath center center no-repeat; width: 100%; height: 60px; + @extend .text-muted; + @extend .text-center; + font-size: 3em; + } + + section { + margin-bottom: 2px; } /* diff --git a/src/app/visualize/visualize.tpl.html b/src/app/visualize/visualize.tpl.html index a5003c80..b7daf1a0 100644 --- a/src/app/visualize/visualize.tpl.html +++ b/src/app/visualize/visualize.tpl.html @@ -9,7 +9,7 @@

    Audio distribution

    -
    +

    Audio distribution for {{:: p.name}} @@ -48,7 +48,9 @@

    Audio distribution for
    -
    +
    + +
    diff --git a/src/baw.paths.nobuild.js b/src/baw.paths.nobuild.js index 75db5b22..5e5141b3 100644 --- a/src/baw.paths.nobuild.js +++ b/src/baw.paths.nobuild.js @@ -58,6 +58,7 @@ module.exports = function (environment) { "analysisResults": { "system": "/analysis_jobs/system/audio_recordings/{recordingId}", "job": "/analysis_jobs/{analysisJobId}/audio_recordings/{recordingId}", + "jobWithPath": "/analysis_jobs/{analysisJobId}/audio_recordings{path}" } }, "links": { @@ -83,11 +84,10 @@ module.exports = function (environment) { // The following intentionally are not prefixed with a '/' // static files "files": { - "error404": "error/error_404.tpl.html", + "error404": "error/error404.tpl.html", "home": "home/home.tpl.html", "login": { - "loginWidget": "login/widget/loginWidget.tpl.html", - "defaultImage": "assets/img/user_spanhalf.png" + "loginWidget": "login/widget/loginWidget.tpl.html" }, "listen": "listen/listen.tpl.html", "annotationViewer": "annotationViewer/annotationViewer.tpl.html", @@ -97,7 +97,11 @@ module.exports = function (environment) { "list": "annotationLibrary/annotationLibrary.tpl.html", "item": "annotationLibrary/annotationItem.tpl.html" }, - "navigation": "navigation/navigation.tpl.html", + "navigation": { + "crumbs": "navigation/navigation.tpl.html", + "left": "navigation/leftNavBar.tpl.html", + "right": "navigation/rightNavBar.tpl.html" + }, "birdWalk": { "list": "birdWalks/birdWalks.tpl.html", "detail": "birdWalks/birdWalk.tpl.html", @@ -118,7 +122,26 @@ module.exports = function (environment) { "distributionVisualisation": "d3Bindings/eventDistribution/distributionVisualisation.tpl.html" } }, - "visualize": "visualize/visualize.tpl.html" + "visualize": "visualize/visualize.tpl.html", + "jobs": { + details: "jobs/details/jobDetails.tpl.html", + list: "jobs/list/jobsList.tpl.html", + "new": "jobs/new/jobNew.tpl.html" + }, + "analysisResults": { + "fileList": "analysisResults/fileList/fileList.tpl.html" + }, + "users": { + "userTile": "users/userTile.tpl.html" + }, + "savedSearches": { + "new": "savedSearches/widgets/newSavedSearch.tpl.html", + "list": "savedSearches/widgets/listSavedSearches.tpl.html", + "show": "savedSearches/widgets/showSavedSearch.tpl.html" + }, + "scripts": { + "show": "scripts/widgets/showScript.tpl.html" + } }, // routes used by angular "ngRoutes": { @@ -132,10 +155,26 @@ module.exports = function (environment) { "d3": "/demo/d3", "rendering": "/demo/rendering", "bdCloud": "/demo/BDCloud2014" + }, + analysisJobs: { + list: "/analysis_jobs", + "new": "/analysis_jobs/new", + details: "/analysis_jobs/{analysisJobId}", + analysisResults: "/analysis_jobs/{analysisJobId}/results:path*?" } }, // general links for use in 's - "links": {} + "links": { + analysisJobs: { + analysisResults: "/analysis_jobs/{analysisJobId}/results", + analysisResultsWithPath: "/analysis_jobs/{analysisJobId}/results{path}" + } + }, + "assets": { + "users": { + "defaultImage": "assets/img/user_spanhalf.png" + } + } } }; @@ -187,9 +226,9 @@ module.exports = function (environment) { var isArray = f instanceof Array; if (isArray) { - wasAnyArray = true; - f.forEach(function(item, index) { - path.push(processFragment(item, i === (fragments.length - 1))); + wasAnyArray = true; + f.forEach(function (item, index) { + path.push(processFragment(item, i === (fragments.length - 1))); }); } else { @@ -229,6 +268,8 @@ module.exports = function (environment) { recursivePath(paths.api.links, paths.api.root); recursivePath(paths.site.files, paths.site.root); recursivePath(paths.site.ngRoutes, paths.api.root); + recursivePath(paths.site.assets, joinPathFragments(environment.siteRoot, environment.siteDir)); return paths; -}; +} +; diff --git a/src/components/directives/angular-ui/bootstrap/bootstrap.js b/src/components/directives/angular-ui/bootstrap/bootstrap.js index ac22c084..f300969d 100644 --- a/src/components/directives/angular-ui/bootstrap/bootstrap.js +++ b/src/components/directives/angular-ui/bootstrap/bootstrap.js @@ -1,7 +1,8 @@ angular.module( "bawApp.directives.ui.bootstrap", [ - "bawApp.directives.ui.bootstrap.pagination" + "bawApp.directives.ui.bootstrap.pagination", + "bawApp.directives.angular-ui.bootstrap.datepicker.dateRangeValidator" ]); diff --git a/src/components/directives/angular-ui/bootstrap/datepicker/dateRangeValidator.js b/src/components/directives/angular-ui/bootstrap/datepicker/dateRangeValidator.js new file mode 100644 index 00000000..a811efdf --- /dev/null +++ b/src/components/directives/angular-ui/bootstrap/datepicker/dateRangeValidator.js @@ -0,0 +1,143 @@ +angular + .module("bawApp.directives.angular-ui.bootstrap.datepicker.dateRangeValidator", []) + .service("bawDateRangeCache", [ + "$timeout", + function ($timeout) { + let minDateControls = new Set(), + maxDateControls = new Set(); + + let revalidating = false; + + + /** + * For the model to update by simulating a user entering a value. + * The problem with the standard $validate function is that it assigns + * `undefined` to the model value if validation fails. We DON'T want that + * ... we just want to update the validation state + * @param ngModel + */ + function forceUpdate(ngModel) { + ngModel.$$lastCommittedViewValue = null; + ngModel.$commitViewValue(ngModel.$viewValue); + } + + function revalidateAll() { + if (!revalidating) { + revalidating = true; + $timeout(() => { + minDateControls.forEach(forceUpdate); + maxDateControls.forEach(forceUpdate); + revalidating = false; + }); + } + } + + return { + addMinDateControl(control) { + minDateControls.add(control); + }, + addMaxDateControl(control) { + maxDateControls.add(control); + }, + revalidateMinDateControls(m, v) { + revalidateAll(); + }, + revalidateMaxDateControls(m, v) { + revalidateAll(); + } + }; + } + ]) + .directive("bawMinDate", [ + "$parse", + "bawDateRangeCache", + function ($parse, bawDateRangeCache) { + return { + scope: false, + restrict: "A", + require: ["ngModel", "^^form"], + link: function (scope, element, attributes, [ngModel, FormController]) { + + let expression = $parse(attributes.bawMinDate); + + bawDateRangeCache.addMinDateControl(ngModel); + + ngModel.$validators.minDate = function (modelValue, viewValue) { + // if its not a date we don't care + // leave other validators to parse date validity or requiredness + let value = modelValue, + result = false; + + if (!angular.isDate(value) || isNaN(value)) { + result = true; + } + else { + let minDate = expression(scope); + + // we don't want the validator to check when the limit is missing + if (!minDate) { + result = true; + } + else if (value >= minDate) { + result = true; + } + } + + // trigger a validation for the other half of the range controls + bawDateRangeCache.revalidateMaxDateControls(modelValue, viewValue); + + return result; + }; + } + }; + } + ] + ) + .directive("bawMaxDate", [ + "$parse", + "bawDateRangeCache", + function ($parse, bawDateRangeCache) { + return { + scope: false, + restrict: "A", + require: ["ngModel", "^^form"], + link: function (scope, element, attributes, [ngModel, FormController]) { + + let expression = $parse(attributes.bawMaxDate); + + bawDateRangeCache.addMaxDateControl(ngModel); + + ngModel.$validators.maxDate = function (modelValue, viewValue) { + // if its not a date we don't care + // leave other validators to parse date validity or requiredness + let value = modelValue, + result = false; + + if (!angular.isDate(value) || isNaN(value)) { + result = true; + } + else { + let maxDate = expression(scope); + + // we don't want the validator to check when the limit is missing + if (!maxDate) { + result = true; + } + else if (value <= maxDate) { + result = true; + } + } + + // trigger a validation for the other half of the range controls + bawDateRangeCache.revalidateMinDateControls(modelValue, viewValue); + + return result; + }; + + + } + }; + } + ] + ); + diff --git a/src/components/directives/angular-ui/bootstrap/pagination/pagination.js b/src/components/directives/angular-ui/bootstrap/pagination/pagination.js index 8a8b13ed..3a3e18d8 100644 --- a/src/components/directives/angular-ui/bootstrap/pagination/pagination.js +++ b/src/components/directives/angular-ui/bootstrap/pagination/pagination.js @@ -1,12 +1,48 @@ angular.module( "bawApp.directives.ui.bootstrap.pagination", - []) -.run([ + ["ui.bootstrap.pagination"]) + .run([ "$templateCache", function($templateCache) { - // override bootstrap-ui's default template - var newTemplate = $templateCache.get("components/directives/angular-ui/bootstrap/pagination/pagination.tpl.html"); - $templateCache.put("template/pagination/pagination.html", newTemplate); + // add ng-href and remove ng-click + const + targetTemplate = "uib/template/pagination/pagination.html", + pageRegex = /(href).*(?:ng-click="selectPage\(([^,]+), \$event\)")/gm, + replaceString = `ng-href="{{ pagination.href($2) }}" href`; + + var oldTemplate = $templateCache.get(targetTemplate); + + var newTemplate = oldTemplate.replace(pageRegex, replaceString); + + $templateCache.put(targetTemplate, newTemplate); + }]) + .directive("paginationHref", ["$parse", function($parse) { + return { + require: ["uibPagination"], + controller: "UibPaginationController", + controllerAs: "pagination", + replace: true, + link: function(scope, element, attrs, ctrls) { + var paginationCtrl = ctrls[0]; + let parentScope = scope; + + // this is dodgy AF but its the only way i can think of to get + // the instance for which the actual expression is attached! + let parts = attrs.paginationHref.split("."); + let parent = parentScope; + if (parts.length > 1) { + parts.splice(-1, 1); + let parentExpression = parts.join("."); + parent = $parse(parentExpression)(parentScope); + } + + let f = $parse(attrs.paginationHref)(parentScope); + paginationCtrl.href = function (...args) { + return f.apply(parent, args); + }; + } + }; }]); + diff --git a/src/components/directives/angular-ui/bootstrap/pagination/pagination.tpl.html b/src/components/directives/angular-ui/bootstrap/pagination/pagination.tpl.html deleted file mode 100644 index 61288f5b..00000000 --- a/src/components/directives/angular-ui/bootstrap/pagination/pagination.tpl.html +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/src/components/directives/bawAnnotationViewer.js b/src/components/directives/bawAnnotationViewer.js index f724616b..44458bc7 100644 --- a/src/components/directives/bawAnnotationViewer.js +++ b/src/components/directives/bawAnnotationViewer.js @@ -1,4 +1,7 @@ -var bawds = bawds || angular.module("bawApp.directives", ["bawApp.configuration", "bawApp.directives.ui.bootstrap"]); +var bawds = bawds || angular.module("bawApp.directives", [ + "bawApp.configuration", + "bawApp.directives.ui.bootstrap", + "bawApp.directives.formChildrenHack"]); bawds.directive("bawAnnotationViewer", [ "conf.paths", diff --git a/src/components/directives/directives.js b/src/components/directives/directives.js index 9f1454eb..d4fb6729 100644 --- a/src/components/directives/directives.js +++ b/src/components/directives/directives.js @@ -1,6 +1,7 @@ var bawds = bawds || angular.module("bawApp.directives", [ "bawApp.configuration", - "bawApp.directives.ui.bootstrap" + "bawApp.directives.ui.bootstrap", + "bawApp.directives.formChildrenHack" ]); diff --git a/src/components/directives/drag.js b/src/components/directives/drag.js index 13fe1516..0b952de7 100644 --- a/src/components/directives/drag.js +++ b/src/components/directives/drag.js @@ -36,8 +36,6 @@ ngDragabilly.directive("draggie", dragEnd: angular.noop }; - // TODO: make getStyleProperty a module - var transformProperty = getStyleProperty("transform"); // jshint ignore:line return { restrict: "A", @@ -46,12 +44,13 @@ ngDragabilly.directive("draggie", }, link: function (scope, $element, attributes/*, controller, transcludeFunction*/) { var element = $element[0]; + const transformProperty = typeof element.style.transform === "string" ? "transform" : "WebkitTransform"; scope.options = angular.extend(defaultOptions, scope.options); var draggie = new Draggabilly(element, scope.options); - draggie.on("dragStart", function (draggie, event, pointer) { + draggie.on("dragStart", function (event, pointer) { scope.options.dragStart(scope, draggie, event, pointer); if (scope.options.raiseAngularEvents) { @@ -59,7 +58,7 @@ ngDragabilly.directive("draggie", } }); - draggie.on("dragMove", function (draggie, event, pointer) { + draggie.on("dragMove", function (event, pointer) { scope.options.dragMove(scope, draggie, event, pointer); if (scope.options.raiseAngularEvents) { @@ -73,7 +72,7 @@ ngDragabilly.directive("draggie", element.style[transformProperty] = "translate3d( " + position.x + "px, " + position.y + "px, 0)"; }; - draggie.on("dragEnd", function (draggie, event, pointer) { + draggie.on("dragEnd", function (event, pointer) { if (!scope.options.useLeftTop) { reposition(draggie.element, draggie.position); } diff --git a/src/components/directives/formChildrenHack.js b/src/components/directives/formChildrenHack.js new file mode 100644 index 00000000..a54e7cf4 --- /dev/null +++ b/src/components/directives/formChildrenHack.js @@ -0,0 +1,41 @@ +// attempts to hack a solution together for: +// https://github.com/angular/angular.js/issues/10071 +// and +// https://github.com/angular/angular.js/pull/11023 +// code in this module based on http://stackoverflow.com/questions/25818757/set-angularjs-nested-forms-to-submitted +angular.module( + "bawApp.directives.formChildrenHack", + []) + .directive("form", function () { + return { + restrict: "E", + require: "form", + link: function (scope, elem, attrs, formCtrl) { + //console.debug("formChildrenHack::form::link: Link function run"); + + scope.$watch(function () { + return formCtrl.$submitted; + }, function (submitted) { + //console.debug("formChildrenHack::form::submittedWatch: submit triggered"); + if (submitted) { + scope.$broadcast("$submitted"); + } + }); + } + }; + }) + + .directive("ngForm", function () { + return { + restrict: "EA", + require: "form", + link: function (scope, elem, attrs, formCtrl) { + //console.debug("formChildrenHack::ngForm::link: Link function run"); + + scope.$on("$submitted", function () { + console.debug("formChildrenHack::ngForm::submittedListener: setting submitted", scope); + formCtrl.$setSubmitted(); + }); + } + }; + }); \ No newline at end of file diff --git a/src/components/directives/input[type=range].js b/src/components/directives/input[type=range].js index 49dfb29c..480c95a1 100644 --- a/src/components/directives/input[type=range].js +++ b/src/components/directives/input[type=range].js @@ -1,4 +1,7 @@ -var bawds = bawds || angular.module("bawApp.directives", ["bawApp.configuration", "bawApp.directives.ui.bootstrap"]); +var bawds = bawds || angular.module("bawApp.directives", [ + "bawApp.configuration", + "bawApp.directives.ui.bootstrap", + "bawApp.directives.formChildrenHack"]); bawds.directive("ngSlider", function () { return { diff --git a/src/components/directives/ngAudio.js b/src/components/directives/ngAudio.js index 375872c8..205b314b 100644 --- a/src/components/directives/ngAudio.js +++ b/src/components/directives/ngAudio.js @@ -1,4 +1,7 @@ -var ngAudio = ngAudio || angular.module("bawApp.directives.ngAudio", ["bawApp.configuration", "bawApp.directives.ui.bootstrap"]); +var ngAudio = ngAudio || angular.module("bawApp.directives.ngAudio", [ + "bawApp.configuration", + "bawApp.directives.ui.bootstrap", + "bawApp.directives.formChildrenHack"]); ngAudio.constant("ngAudioEvents", { diff --git a/src/components/directives/ngEval.js b/src/components/directives/ngEval.js index ab506d22..2b8d9ba3 100644 --- a/src/components/directives/ngEval.js +++ b/src/components/directives/ngEval.js @@ -1,4 +1,7 @@ -var bawds = bawds || angular.module("bawApp.directives", ["bawApp.configuration", "bawApp.directives.ui.bootstrap"]); +var bawds = bawds || angular.module("bawApp.directives", [ + "bawApp.configuration", + "bawApp.directives.ui.bootstrap", + "bawApp.directives.formChildrenHack"]); bawds.directive("ngEval", function () { return { diff --git a/src/components/directives/ngGoogleMaps.js b/src/components/directives/ngGoogleMaps.js index e35e2c42..c7dca016 100644 --- a/src/components/directives/ngGoogleMaps.js +++ b/src/components/directives/ngGoogleMaps.js @@ -4,7 +4,11 @@ /* globals google*/ -var bawds = bawds || angular.module("bawApp.directives", ["bawApp.configuration", "bawApp.directives.ui.bootstrap"]); +var bawds = bawds || angular.module("bawApp.directives", [ + "bawApp.configuration", + "bawApp.directives.ui.bootstrap", + "bawApp.directives.formChildrenHack" + ]); /* Start map directives */ /** stolen from angular ui diff --git a/src/components/filters/filters.js b/src/components/filters/filters.js index 91d5ee24..38624a26 100644 --- a/src/components/filters/filters.js +++ b/src/components/filters/filters.js @@ -1,105 +1,109 @@ - /* http://docs.angularjs.org/#!angular.filter */ - var bawfs = bawfs || angular.module("bawApp.filters", []); - - /* - http://stackoverflow.com/questions/11873570/angularjs-for-loop-with-numbers-ranges - -
    - do something -
    - */ - bawfs.filter("range", function() { - return function(input, total) { - total = baw.parseInt(total); - for (var i=0; i + do something +

    + */ +bawfs.filter("range", function () { + return function (input, total) { + total = baw.parseInt(total); + for (var i = 0; i < total; i++) { + input.push(i); + } + return input; + }; +}); + +bawfs.filter("boolToWords", function () { + return function (text, truePhrase, falsePhrase) { + var value = JSON.parse(text); + if (value) { + return truePhrase || ""; + } + else { + return falsePhrase || ""; + } + }; +}); + +/** + * moment js adapters + * + * requires momentjs + */ +bawfs.filter("moment", ["moment", function (moment) { + return function (input, method) { + + if (input) { + var restOfArguments = Array.prototype.slice.call(arguments, 2, arguments.length); + + var m = moment(input); + return m[method].apply(m, restOfArguments); + + } + + return ""; + }; +}]); + + +/** + * Format a given value to the with the site's default timespan formatter + * assumes input is in seconds + */ +bawfs.filter("formatTimeSpan", function () { + return function (input) { + + if (input) { + return baw.secondsToDurationFormat(input); + } + else { + return ""; + } + + }; +}); + + +/** + * Output a tag name when given an ID + */ +bawfs.filter("tagName", ["Tag", function (Tag) { + return function (input) { + + var id = parseInt(input, 10); + + if (id && !isNaN(id)) { + var tag = Tag.resolve(id); + + if (tag) { + return tag.text; } - else { - return ""; - } - - }; - }); - - - /** - * Output a tag name when given an ID - */ - bawfs.filter("tagName", ["Tag", function(Tag) { - return function(input) { - - var id = parseInt(input, 10); - - if (id && !isNaN(id)) { - var tag = Tag.resolve(id); - - if (tag) { - return tag.text; - } - - return ""; - } - else { - return ""; - } - }; - }]); - - bawfs.filter("format", function() { - return function stringFormatFilter(string, args) { - if (angular.isString(string)) { - return String.format.apply(string, (arguments)); - } - - throw "A string is required for the first argument"; - }; - }); + return ""; + } + else { + return ""; + } + }; +}]); + +bawfs.filter("format", function () { + return function stringFormatFilter(string, args) { + if (angular.isString(string)) { + return String.format.apply(string, (arguments)); + } + + throw "A string is required for the first argument"; + }; +}); + +bawfs.filter("percentage", ["$filter", function ($filter) { + return function (input, decimals = 2) { + return $filter("number")(input * 100, decimals) + "%"; + }; +}]); \ No newline at end of file diff --git a/src/components/models/analysisJob.js b/src/components/models/analysisJob.js new file mode 100644 index 00000000..c9796787 --- /dev/null +++ b/src/components/models/analysisJob.js @@ -0,0 +1,152 @@ +angular + .module("bawApp.models.analysisJob", []) + .constant("baw.models.AnalysisJob.progressKeys", { + "queued": "queued", + "working": "working", + "successful": "successful", + "failed": "failed", + "total": "total" + }) + .constant("baw.models.AnalysisJob.statusKeys", { + "new": "new", + "preparing": "preparing", + "processing": "processing", + "suspended": "suspended", + "completed": "completed" + }) + .factory("baw.models.AnalysisJob", [ + "baw.models.associations", + "baw.models.ApiBase", + "baw.models.AnalysisJob.progressKeys", + "baw.models.AnalysisJob.statusKeys", + "UserProfile", + "conf.paths", + "$url", + "humanize-duration", + "filesize", + "moment", + function (associations, ApiBase, keys, statusKeys, UserProfile, paths, $url, humanizeDuration, filesize, moment) { + + class AnalysisJob extends ApiBase { + constructor(resource) { + super(resource); + + this.customSettings = this.customSettings || null; + this.overallStatusModifiedAt = new Date(this.overallStatusModifiedAt); + this.overallProgressModifiedAt = new Date(this.overallProgressModifiedAt); + this.overallCount = Number(this.overallCount); + this.overallDurationSeconds = Number(this.overallDurationSeconds); + this.overallSizeBytes = this.overallSizeBytes || null; + this.overallStatus = this.overallStatus || null; + this.overallProgress = this.overallProgress || null; + this.savedSearchId = Number(this.savedSearchId); + this.scriptId = this.scriptId || null; + this.startedAt = new Date(this.startedAt); + } + + get isNew() { + return this.overallStatus === statusKeys.new; + } + + get isPreparing() { + return this.overallStatus === statusKeys.preparing; + } + + get isProcessing() { + return this.overallStatus === statusKeys.processing; + } + + get isSuspended() { + return this.overallStatus === statusKeys.suspended; + } + + get isCompleted() { + return this.overallStatus === statusKeys.completed; + } + + get isActive() { + return this.isNew || this.isPreparing || this.isProcessing; + } + + get completedRatio() { + return ((this.overallProgress.successful || 0) + (this.overallProgress.failed || 0)) / this.overallCount; + } + + get successfulRatio() { + return (this.overallProgress.successful || 0) / this.overallCount; + } + + + get friendlyDuration() { + return humanizeDuration(this.overallDurationSeconds * 1000, {largest: 2}); + } + + get friendlyRunningTime() { + let lastUpdate = Math.max(+this.overallProgressModifiedAt, +this.overallStatusModifiedAt), + delta = +lastUpdate - +this.createdAt; + + return moment.duration(delta).humanize(); + } + + + get friendlySize() { + if (this.overallSizeBytes) { + return filesize(this.overallSizeBytes, {round: 0}); + } + else { + return "unknown"; + } + } + + get friendlyUpdated() { + var lastUpdate = Math.max(this.overallProgressModifiedAt, this.overallStatusModifiedAt); + + return moment(lastUpdate).fromNow(); + } + + get resultsUrl() { + return $url.formatUri( + paths.site.links.analysisJobs.analysisResults, + {analysisJobId: this.id} + ); + } + + get viewUrl() { + return $url.formatUri( + paths.site.ngRoutes.analysisJobs.details, + {analysisJobId: this.id} + ); + } + + static get viewListUrl() { + return $url.formatUri(paths.site.ngRoutes.analysisJobs.list); + } + + + generateSuggestedName() { + //let currentUserName = !!UserProfile.profile ? UserProfile.profile.userName : "(unknown user)"; + let scriptName = !!this.script ? this.script.name : "(not chosen)"; + let savedSearchName = !!this.savedSearch && !!this.savedSearch.name ? this.savedSearch.name : "(not chosen)"; + return `"${scriptName}" analysis run on the "${savedSearchName}" data`; + } + + get savedSearch() { + return this._savedSearch || null; + } + + set savedSearch(value) { + this._savedSearch = value; + } + + get script() { + return this._script || null; + } + + set script(value) { + this._script = value; + } + + } + + return AnalysisJob; + }]); diff --git a/src/components/models/analysisResult.js b/src/components/models/analysisResult.js new file mode 100644 index 00000000..227f18d8 --- /dev/null +++ b/src/components/models/analysisResult.js @@ -0,0 +1,184 @@ +angular + .module("bawApp.models.analysisResult", []) + .factory("baw.models.AnalysisResult", [ + "baw.models.associations", + "baw.models.ApiBase", + + "conf.paths", + "$url", + "humanize-duration", + "filesize", + "moment", + "MimeType", + function (associations, ApiBase, paths, $url, humanizeDuration, filesize, moment, MimeType) { + + class AnalysisResult extends ApiBase { + constructor(resource, parent) { + super(resource); + + this._parent = parent; + this.path = this.path || null; + this.name = this.name || null; + this.type = this.type || null; + this.mime = this.mime || null; + this.sizeBytes = this.sizeBytes || null; + this.hasChildren = this.hasChildren === true || false; + this.hasZip = this.hasZip === true || false; + + let children = []; + if (this.children) { + // recursive! + children = this + .children + .map(x => new AnalysisResult(x, this)) + .sort(AnalysisResult.sort); + } + this.children = children; + } + + get analysisJob() { + // allow linking back to results's parent directory to get analysisJob + // that generated these results + return this._analysisJob || (this._parent && this._parent.analysisJob); + } + + set analysisJob(value) { + this._analysisJob = value; + } + + get analysisJobId() { + // allow linking back to results's parent directory to get analysisJob + // that generated these results + return this._analysisJobId || (this._parent && this._parent.analysisJobId); + } + + set analysisJobId(value) { + this._analysisJobId = value; + } + + get audioRecordingId() { + // allow linking back to results's parent directory to get audioRecordingId + // that generated these results + return this._audioRecordingId || (this._parent && this._parent.audioRecordingId); + } + + set audioRecordingId(value) { + this._audioRecordingId = value; + } + + + get isDirectory() { + return this.type === "directory"; + } + + get isFile() { + return this.type === "file"; + } + + get friendlySize() { + if (this.sizeBytes) { + return filesize(this.sizeBytes, {round: 0}); + } + else { + return ""; + } + } + + get icon() { + if (this.isDirectory) { + return "fa fa-folder-o"; + } + + return MimeType.mimeToFaIcon(this.mime); + } + + get path() { + // allow linking back to results's parent directory to get path + // that generated these results + + if (this._path) { + return this._path; + } + + if (!this._parent) { + return undefined; + } + + let path = this._parent.path; + + if (!path) { + return undefined; + } + + if (!path.endsWith("/")) { + path = path + "/"; + } + + return path + this.name; + } + + set path(value) { + this._path = value; + } + + + // url to the resource + get url() { + let analysisJobId = !this.analysisJob ? this.analysisJobId : this.analysisJob.id; + + let url = paths.api.routes.analysisResults.jobWithPath; + + let result = $url.formatUri( + url, + {analysisJobId, path: this.path} + ); + + return result; + } + + get zipUrl() { + return this.url + ".zip"; + } + + get viewUrl() { + let analysisJobId = !this.analysisJob ? this.analysisJobId : this.analysisJob.id; + + let url = paths.site.links.analysisJobs.analysisResultsWithPath; + + let result = $url.formatUri( + url, + {analysisJobId, path: this.path} + ); + + return result; + } + + static sort(a, b) { + if (!a) { + return -1; + } + + if (!b) { + return 1; + } + + let aDir = a.isDirectory, + bDir = b.isDirectory; + + if (aDir && !bDir) { + return -1; + } + + if (!aDir && bDir) { + return 1; + } + + // if both not dirs, or if both dirs, sort on name + return a.name.localeCompare(b.name); + } + + + } + + return AnalysisResult; + }]); diff --git a/src/components/models/annotation.spec.js b/src/components/models/annotation.spec.js index 4d592210..d299d60f 100644 --- a/src/components/models/annotation.spec.js +++ b/src/components/models/annotation.spec.js @@ -111,7 +111,7 @@ describe("The Annotation object", function () { }); it("'s prototype should have all of the resource properties defined", function () { - expect(baw.Annotation.prototype).toImplement({isNew: null, mergeResource: null, exportObj: null}); + expect(baw.Annotation.prototype).toImplement({isNew: Function, mergeResource: Function, exportObj: Function}); }); diff --git a/src/components/models/associations.js b/src/components/models/associations.js index 644adea2..8ea5897c 100644 --- a/src/components/models/associations.js +++ b/src/components/models/associations.js @@ -15,6 +15,33 @@ angular constructor(resource) { Object.assign(this, resource); + + // createdAt and UpdatedAt are fairly common attributes + if (this.createdAt) { + this.createdAt = new Date(this.createdAt); + } + + if (this.updatedAt) { + this.updatedAt = new Date(this.updatedAt); + } + + if (this.creatorId) { + this.creatorId = Number(this.creatorId); + } + + if (this.updaterId) { + this.updaterId = Number(this.updaterId); + } + } + + /** + * If called, auto downloads linked resources. + * Since we wabt customiseable behaviour, we force an explicit call + * to autoDownload. + * @param resources + */ + autoDownload(resources) { + } static make(resource) { @@ -54,6 +81,12 @@ angular return response; } + + static makeFromApiWithType(Type) { + return function (resource) { + return ApiBase.makeFromApi.call(Type, resource); + }; + } } return ApiBase; @@ -97,17 +130,24 @@ angular parentManyRelationSuffix = "Id" + pluralitySuffix, id = "id", arityMany = Symbol("many"), - //arityOne = Symbol("one"), - unavailable = "This parent resource is unavailable."; + arityOne = Symbol("one"), + unavailable = "This parent resource is unavailable.", + undefinedToUnavailable = x => x === undefined ? new ModelUnavailable(unavailable) : x, + linkerCache = new Map(); + + var getName = (n) => n instanceof Object ? n.name : n; + var getArity = (n) => n instanceof Object ? n.arity : arityOne; function many(name) { - return { - name, - arity: arityMany - }; + return {name, arity: arityMany}; } - var associations = new Map([ + function one(name) { + return {name, arity: arityOne}; + } + + + var associations = Object.freeze(new Map([ [ "Tag", { parents: null, children: [many("Tagging")] @@ -145,8 +185,20 @@ angular [ "User", { parents: null, children: [many("Bookmark")] + }], + [ + "SavedSearch", { + parents: [many("AnalysisJob")], children: null + }], + [ + "Script", { + parents: [many("AnalysisJob")], children: null + }], + [ + "AnalysisJob", { + parents: null, children: [one("Script"), one("SavedSearch")] }] - ]); + ])); function chainToString(chain) { @@ -156,13 +208,65 @@ angular return { generateLinker, arrayToMap, - makeFromApi (Type) { - return function (resource) { - return ApiBase.makeFromApi.call(Type, resource); - }; - } + associations, + autoDownload }; + + function autoDownload(targetType, arity = arityOne, limit = []) { + throw new Error("Not Implemented"); + /* + // get associated models + let targetAssociation = associations.get(targetType), + models = []; + if (targetAssociation.parents) { + models = models.concat(targetAssociation.parents); + } + if (targetAssociation.children) { + models = models.concat(targetAssociation.children); + } + + models = models + .filter(p => arity === arityMany || getArity(p) === arityOne) + .filter(p => limit.length === 0 || limit.indexOf(getName(p)) >= 0); + + // get linkers + let linkers = models.map( model => generateLinker(targetType, model) ); + + // TODO: this won't work until services are expressed as Service functions + let getService = () => { }; + + for (let i = 0; i < models.length; i++) { + + + let model = models[i], + arity = getArity(model), + service = getService(getName(model)); + + if (arity === arityMany) { + service.filter() + } + else { + service.get() + } + } + */ + } + + + + function isManyAssociation(previousAssociation, currentAssociation) { + let p = previousAssociation.parents || [], + c = previousAssociation.children || []; + + let a = p.concat(c).find(x => getName(x) === currentAssociation); + if (!a) { + return false; + } + + return getArity(a) === arityMany; + } + /** * This function determines if there is a way to link * a child to a parent node. If there is, it returns a function @@ -170,6 +274,11 @@ angular */ function generateLinker(child, parent) { + let cacheKey = child + "---->" + parent; + if (linkerCache.has(cacheKey)) { + return linkerCache.get(cacheKey); + } + if (!associations.has(child)) { throw new Error("Child must be one of the known associations"); } @@ -197,7 +306,7 @@ angular console.debug("associations:generateLinker:", chainToString(chain)); // now make an optimised function to execute it - return function (target, associationCollections) { + let linker = function (target, associationCollections) { var currentTargets = [target]; for (let c = 1; c < chain.length; c++) { let association = chain[c], @@ -207,6 +316,9 @@ angular targetName = correctCase[c] + (manyTargets ? pluralitySuffix : ""); // get the collection appropriate for the first association + if (!associationCollections.hasOwnProperty(association)) { + throw new Error(`No associations Map supplied for model ${association}`); + } let possibleParentObjects = associationCollections[association]; // when following many arity associations, there may be more than one @@ -250,8 +362,7 @@ angular // handle the cases of missing associations // this can sometimes happen when certain associations are // filtered out from a dataset for security reasons - realAssociations = realAssociations.map( - x => x === undefined ? new ModelUnavailable(unavailable) : x); + realAssociations = realAssociations.map(undefinedToUnavailable); // assign to child currentTarget[targetName] = manyTargets ? realAssociations : realAssociations[0]; @@ -266,17 +377,8 @@ angular return target; }; - function isManyAssociation(previousAssociation, currentAssociation) { - let p = previousAssociation.parents || [], - c = previousAssociation.children || []; - - let a = p.concat(c).find(x => x.name === currentAssociation); - if (!a) { - return false; - } - - return a.arity === arityMany; - } + linkerCache.set(cacheKey, linker); + return linker; } /** @@ -306,7 +408,7 @@ angular for (var i = 0; i < nodesToVisit.length; i++) { var n = nodesToVisit[i]; - let thisNode = n instanceof Object ? n.name : n; + let thisNode = getName(n); // prevent cyclic loops // if the new node has already been visited, @@ -342,7 +444,7 @@ angular */ function arrayToMap(items) { return new Map( - [for (item of items) [item[id], item]] + items.map(item => [item[id], item]) ); } diff --git a/src/components/models/models.js b/src/components/models/models.js index 24216f2f..9dc3d47f 100644 --- a/src/components/models/models.js +++ b/src/components/models/models.js @@ -5,8 +5,10 @@ angular.module( "rails", "bawApp.services", "bawApp.models.associations", -// + // // endpoint specific + "bawApp.models.analysisJob", + "bawApp.models.analysisResult", //"bawApp.models.bookmark", "bawApp.models.project", "bawApp.models.site", @@ -16,6 +18,8 @@ angular.module( //"bawApp.models.taggings", "bawApp.models.tag", "bawApp.models.media", + "bawApp.models.savedSearch", + "bawApp.models.script", //"bawApp.models.birdWalkService", //"bawApp.models.breadcrumbs", "bawApp.models.userProfile", diff --git a/src/components/models/savedSearch.js b/src/components/models/savedSearch.js new file mode 100644 index 00000000..2ce9b3c8 --- /dev/null +++ b/src/components/models/savedSearch.js @@ -0,0 +1,142 @@ +angular + .module("bawApp.models.savedSearch", []) + .factory("baw.models.SavedSearch", [ + "baw.models.associations", + "baw.models.ApiBase", + "conf.paths", + "$url", + "humanize-duration", + "moment", + "QueryBuilder", + function (associations, ApiBase, paths, $url, humanizeDuration, moment, QueryBuilder) { + + /** + * Represents a saved filter and its settings. + * The storedQuery is a QueryBuilder filter as an object. + * The storedQuery is executed against AudioRecordings + */ + class SavedSearch extends ApiBase { + constructor(resource) { + //let model = this; + + super(resource); + + this._storedQuery = {}; + + this.id = this.id || null; + this.name = this.name || null; + this.description = this.description || null; + this.storedQuery = this.storedQuery || {}; + this.projectIds = this.projectIds || null; + this.analysisJobIds = this.analysisJobIds || null; + + // client only fields + + + if (resource) { + this.basicFilter = undefined; + } + else { + // only when new'ed on client side + + // ensure properties added here are taken care of + // in `updateQueryFromBasicFilter` as well. + let basicFilterBase = { + projectId: null, + siteIds: [], + minimumDate: null, + maximumDate: null + }; + + this.basicFilter = basicFilterBase; + } + } + + get friendlyUpdated() { + var lastUpdate = this.createdAt; + + return moment(lastUpdate).fromNow(); + } + + + + generateSuggestedName(projects, sites) { + if (!this.basicFilter) { + return undefined; + } + + let project = projects.find(p => p.id === this.basicFilter.projectId); + + let projectName = !!project ? project.name : "(no project)"; + + let chosenSites = sites.filter(s => this.basicFilter.siteIds.indexOf(s.id) >=0).map(s => s.name); + + let siteName = "(no sites)"; + if (sites && this.basicFilter.siteIds.length > 0) { + siteName = this.basicFilter.siteIds.length === sites.length ? "All sites" : "Sites " + chosenSites.join(", "); + } + + let dates = "", + min = moment(this.basicFilter.minimumDate).format("YYYY-MMM-DD"), + max = moment(this.basicFilter.maximumDate).format("YYYY-MMM-DD"); + if (this.basicFilter.minimumDate && this.basicFilter.maximumDate) { + dates = ` between ${min} and ${max}`; + } + else if (this.basicFilter.minimumDate) { + dates = ` ending after ${min}`; + } + else if (this.basicFilter.maximumDate) { + dates = ` starting before ${max}`; + } + + return `${siteName} in ${projectName}${dates}`; + } + + /** + * Convert basic filter object graph into a + * QueryBuilder query. Presently needs to be called + * manually since es6 proxies don't exist :-( + */ + updateQueryFromBasicFilter() { + let filter = this.basicFilter; + + // query executed against audio recordings + var query = QueryBuilder.create(function(baseQuery) { + let q = baseQuery; + + if (filter.projectId) { + q = q.eq("projects.id", filter.projectId); + } + + if (filter.siteIds.length > 0) { + q = q.in("siteId", filter.siteIds); + } + + if (filter.minimumDate) { + q = q.gt("recordedDate", filter.minimumDate); + } + + if (filter.maximumDate) { + // NB: recordedEndDate does not currently exist. + q = q.lt("recordedEndDate", filter.maximumDate); + } + + return q; + }); + + this._storedQuery = query.toJSON(); + } + + get storedQuery() { + return this._storedQuery; + } + + set storedQuery(value) { + // TODO: querybuilder validate + this._storedQuery = value; + } + + } + + return SavedSearch; + }]); diff --git a/src/components/models/scripts.js b/src/components/models/scripts.js new file mode 100644 index 00000000..b64affd2 --- /dev/null +++ b/src/components/models/scripts.js @@ -0,0 +1,37 @@ +angular + .module("bawApp.models.script", []) + .factory("baw.models.Script", [ + "baw.models.associations", + "baw.models.ApiBase", + "conf.paths", + "$url", + "humanize-duration", + "moment", + function (associations, ApiBase, paths, $url, humanizeDuration, moment) { + + class Script extends ApiBase { + constructor(resource) { + super(resource); + + + this.version = Number(this.version); + + this.executableSettings = this.executableSettings || null; + this.executableSettingsMediaType = this.executableSettingsMediaType || null; + + + } + + + get friendlyUpdated() { + var lastUpdate = this.createdAt; + + return moment(lastUpdate).fromNow(); + } + + + + } + + return Script; + }]); diff --git a/src/components/models/tag.js b/src/components/models/tag.js index e709a4b2..fc577cb4 100644 --- a/src/components/models/tag.js +++ b/src/components/models/tag.js @@ -3,11 +3,11 @@ angular .factory( "baw.models.Tag", [ - "baw.models.associations", + "baw.models.ApiBase", "conf.paths", "Authenticator", "$url", - function (associations, paths, Authenticator, url) { + function (ApiBase, paths, Authenticator, url) { function Tag(resourceOrNewTag) { @@ -95,7 +95,7 @@ angular return new Tag(value); }; - Tag.makeFromApi = associations.makeFromApi(Tag); + Tag.makeFromApi = ApiBase.makeFromApiWithType(Tag); return Tag; }]); diff --git a/src/components/models/tag.spec.js b/src/components/models/tag.spec.js index dcb3ea60..429a578a 100644 --- a/src/components/models/tag.spec.js +++ b/src/components/models/tag.spec.js @@ -8,13 +8,25 @@ describe("The Tag object", function () { "creatorId": 7, "id": 1, "isTaxanomic": false, - "notes": null, + "notes": {}, "retired": false, "text": "Corvus Orru", "typeOfTag": "species_name", "updatedAt": "2013-11-20T13:19:13Z", "updaterId": 7 }; + var resourceTypes = { + "createdAt": Date, + "creatorId": Number, + "id": Number, + "isTaxanomic": Boolean, + "notes": Object, + "retired": Boolean, + "text": String, + "typeOfTag": String, + "updatedAt": Date, + "updaterId": Number + }; beforeEach(module("bawApp.models", "rails")); @@ -46,9 +58,13 @@ describe("The Tag object", function () { expect(f).toThrow(); }); - it("should expose all the resource with its own api", function () { - expect(existingTag).toImplement(resource); - expect(newTag).toImplement(resource); + it("should expose all the resource with its own api - with an existing resource", function () { + expect(existingTag).toImplement(resourceTypes); + }); + + // jasmineMatchers' toImplement current does not support testing for fields with null values + xit("should expose all the resource with its own api - with a new resource", function () { + expect(newTag).toImplement(resourceTypes); }); var dateFields = [ diff --git a/src/components/models/userProfile.js b/src/components/models/userProfile.js index 4557190c..a4dcc351 100644 --- a/src/components/models/userProfile.js +++ b/src/components/models/userProfile.js @@ -6,17 +6,14 @@ angular "baw.models.associations", "baw.models.ApiBase", "conf.paths", + "conf.constants", "$url", - function (associations, ApiBase, paths, $url) { + function (associations, ApiBase, paths, constants, $url) { class UserProfile extends ApiBase { - constructor(resource, defaultProfile) { - if (!defaultProfile) { - throw new Error("A default profile must be supplied"); - } - + constructor(resource) { if (!resource) { - resource = defaultProfile; + resource = constants.defaultProfile; } super(resource); @@ -24,7 +21,7 @@ angular this.preferences = this.preferences || {}; // ensure preferences are always updated - this.preferences = Object.assign({}, defaultProfile.preferences, this.preferences); + this.preferences = Object.assign({}, constants.defaultProfile.preferences, this.preferences); this.imageUrls = this.imageUrls.reduce((s, c) => { c.url = paths.api.root + c.url; diff --git a/src/components/services/analysisJob.js b/src/components/services/analysisJob.js new file mode 100644 index 00000000..bf23f5f8 --- /dev/null +++ b/src/components/services/analysisJob.js @@ -0,0 +1,169 @@ +angular + .module("bawApp.services.analysisJob", []) + .factory( + "AnalysisJob", + [ + "$resource", + "bawResource", + "$http", + "$q", + "conf.paths", + "lodash", + "casingTransformers", + "QueryBuilder", + "baw.models.AnalysisJob", + function ($resource, bawResource, $http, $q, paths, _, casingTransformers, QueryBuilder, AnalysisJobModel) { + + // FAKED! + let fakedData = [ + { + "id": 11111, + "name": "fake 11111 new fake", + "annotation_name": null, + "custom_settings": "#custom settings 267", + "script_id": 1, + "creator_id": 144, + "updater_id": 144, + "deleter_id": null, + "deleted_at": null, + "created_at": "2016-01-18 06:03:10.047508", + "updated_at": "2016-02-01 06:03:10.093619", + "description": null, + "saved_search_id": 1, + "started_at": "2016-02-18 06:03:10.028024", + "overall_status": "new", + "overall_status_modified_at": "2016-02-18 06:03:10.028276", + "overall_progress": {}, + "overall_progress_modified_at": "2016-02-18 06:03:10.028776", + "overall_count": 66, + "overall_duration_seconds": 6600 + + }, + { + "id": 22222, + "name": "fake 22222 fake fake 22222 fake fake 22222 fake ", + "annotation_name": null, + "custom_settings": "#custom settings 267", + "script_id": 1, + "creator_id": 9, + "updater_id": 144, + "deleter_id": null, + "deleted_at": null, + "created_at": "2016-01-18 06:03:10.047508", + "updated_at": "2016-02-01 06:03:10.093619", + "description": null, + "saved_search_id": 1, + "started_at": "2016-02-18 06:03:10.028024", + "overall_status": "preparing", + "overall_status_modified_at": "2016-02-18 06:03:10.028276", + "overall_progress": {}, + "overall_progress_modified_at": "2016-02-18 06:03:10.028776", + "overall_count": 77, + "overall_duration_seconds": 77700 + + }, + { + "id": 1, + "name": "\"simulate work analysis\" run on the \"All sites in SERF Acoustic Study\" data", + "annotation_name": null, + "custom_settings": "#custom settings 267", + "script_id": 1, + "creator_id": 9, + "updater_id": 9, + "deleter_id": null, + "deleted_at": null, + "created_at": "2016-02-18 06:03:10.047508", + "updated_at": "2016-02-18 06:03:10.093619", + "description": null, + "saved_search_id": 1, + "started_at": "2016-02-18 06:03:10.028024", + "overall_status": "processing", + "overall_status_modified_at": "2016-02-18 06:03:10.028276", + "overall_progress": {"queued": 10, "working": 1, "successful": 35, "failed": 4, "total": 0}, + "overall_progress_modified_at": (new Date()).setMinutes(0, 0, 0), + "overall_count": 50, + "overall_duration_seconds": 88888, + "overall_size_bytes": 123456789 + }, + { + "id": 3600, + "name": "fake 3600 completed fake", + "annotation_name": null, + "custom_settings": "#custom settings 267", + "script_id": 1, + "creator_id": 9, + "updater_id": 144, + "deleter_id": null, + "deleted_at": null, + "created_at": "2016-01-18 06:03:10.047508", + "updated_at": "2016-02-01 06:03:10.093619", + "description": null, + "saved_search_id": 1, + "started_at": "2016-02-18 06:03:10.028024", + "overall_status": "completed", + "overall_status_modified_at": "2016-02-18 06:03:10.028276", + "overall_progress": {"queued": 0, "working": 0, "successful": 90, "failed": 10, "total": 100}, + "overall_progress_modified_at": "2016-02-18 06:03:10.028776", + "overall_count": 100, + "overall_duration_seconds": 100 * 3600 * 2, + "overall_size_bytes": 123456789 + + }, + + { + "id": 99999, + "name": "fake 99999 suspended fake", + "annotation_name": null, + "custom_settings": "#custom settings 267", + "script_id": 1, + "creator_id": 9, + "updater_id": 144, + "deleter_id": null, + "deleted_at": null, + "created_at": "2016-01-18 06:03:10.047508", + "updated_at": "2016-02-01 06:03:10.093619", + "description": null, + "saved_search_id": 1, + "started_at": "2016-02-18 06:03:10.028024", + "overall_status": "suspended", + "overall_status_modified_at": "2016-02-18 06:03:10.028276", + "overall_progress": {"queued": 10, "working": 0, "successful": 80, "failed": 10, "total": 100}, + "overall_progress_modified_at": "2016-02-18 06:03:10.028776", + "overall_count": 99, + "overall_duration_seconds": 99999 + + } + + ]; + fakedData = casingTransformers.transformObject(fakedData, casingTransformers.camelize); + + function query() { + //const path = paths.api.routes.analysisResults; + return $q.when({data: {data: fakedData}}) + .then(x => AnalysisJobModel.makeFromApi(x)); + } + + function get(id) { + return $q.when({data: {data: fakedData.find(x => x.id === id)}}) + .then(x => AnalysisJobModel.makeFromApi(x)); + } + + function getName(id) { + let fake = fakedData.find(x => x.id === id); + return $q.when({ + data: { + data: { + id: fake.id, + name: fake.name + } + } + }) + .then(x => AnalysisJobModel.makeFromApi(x)); + } + + return { + query, + get, + getName + }; + }]); diff --git a/src/components/services/analysisResult/analysisResult.js b/src/components/services/analysisResult/analysisResult.js index 8a0b109f..3fa4adc7 100644 --- a/src/components/services/analysisResult/analysisResult.js +++ b/src/components/services/analysisResult/analysisResult.js @@ -1,16 +1,496 @@ angular .module("bawApp.services.analysisResult", []) .factory( - "AnalysisResult", - [ - "$http", - function ($http) { - var analysisResult = {}; + "AnalysisResult", + [ + "$resource", + "bawResource", + "$http", + "$q", + "conf.paths", + "lodash", + "casingTransformers", + "QueryBuilder", + "baw.models.AnalysisResult", + function ($resource, + bawResource, + $http, + $q, + paths, + _, + casingTransformers, + QueryBuilder, + AnalysisResultModel) { + // FAKED! + let fakedData = [ + { + "analysis_job_id": 1, + "audio_recording_id": null, + "path": "/", + "name": "/", + "type": "directory", + "has_zip": false, + "children": [ + 1234, + 123456, + 124124, + 234234, + ...(new Array(1000)).fill(1).map((x, i) => i) + ].map(x => ({ + name: x.toString(), + path: "/" + x.toString(), + type: "directory", + "has_children": true, + has_zip: false + })) + }, + { + "analysis_job_id": 1, + "audio_recording_id": 1234, + "path": "/1234", + "name": "1234", + "type": "directory", + "has_zip": true, + "children": [ + { + "name": "log.txt", + "type": "file", + "size_bytes": 196054, + "mime": "text/plain" + }, + { - return analysisResult; - } - ] -); \ No newline at end of file + "name": "Towsey.Acoustic.yml", + "type": "file", + "size_bytes": 1968, + "mime": "application/x-yaml" + }, + { + "path": "/1234/Towsey.Acoustic", + "name": "Towsey.Acoustic", + "type": "directory", + "has_children": true + } + ] + + }, + { + "analysis_job_id": 1, + "audio_recording_id": 1234, + "path": "/1234/Towsey.Acoustic/Hello/test/bigtest/test.txt", + "name": "test.txt", + "type": "directory", + "children": [] + }, + { + "analysis_job_id": 1, + "audio_recording_id": 1234, + "path": "/1234/Towsey.Acoustic/ZoomingTiles", + "name": "ZoomingTiles", + "type": "directory", + "has_zip": false, + "children": [] + }, + { + + "analysis_job_id": 1, + "audio_recording_id": 1234, + "path": "/1234/Towsey.Acoustic", + "name": "Towsey.Acoustic", + "type": "directory", + "has_zip": true, + "children": [ + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__IndexGenerationData.json", + "type": "file", + "size_bytes": 217, + "mime": "application/json" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__IndexStatistics.json", + "type": "file", + "size_bytes": 12549, + "mime": "application/json" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__IndexDistributions.png", + "type": "file", + "size_bytes": 21726, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI.png", + "type": "file", + "size_bytes": 394539, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__BGN.png", + "type": "file", + "size_bytes": 271785, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__CVR.png", + "type": "file", + "size_bytes": 406413, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__DIF.png", + "type": "file", + "size_bytes": 10799, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ENT.png", + "type": "file", + "size_bytes": 397892, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__EVN.png", + "type": "file", + "size_bytes": 420567, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__SUM.png", + "type": "file", + "size_bytes": 6001, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__POW.png", + "type": "file", + "size_bytes": 386530, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__SPT.png", + "type": "file", + "size_bytes": 332683, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__BGN-POW-CVR.png", + "type": "file", + "size_bytes": 830777, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.png", + "type": "file", + "size_bytes": 954437, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__2Maps.png", + "type": "file", + "size_bytes": 1555863, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__BGN-POW-CVR.SummaryRibbon.png", + "type": "file", + "size_bytes": 5367, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.SummaryRibbon.png", + "type": "file", + "size_bytes": 5486, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__BGN-POW-CVR.SpectralRibbon.png", + "type": "file", + "size_bytes": 105946, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.SpectralRibbon.png", + "type": "file", + "size_bytes": 121949, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-140000Z_60.png", + "type": "file", + "size_bytes": 40594, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-150000Z_60.png", + "type": "file", + "size_bytes": 38652, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-160000Z_60.png", + "type": "file", + "size_bytes": 38851, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-170000Z_60.png", + "type": "file", + "size_bytes": 35306, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-180000Z_60.png", + "type": "file", + "size_bytes": 38927, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-190000Z_60.png", + "type": "file", + "size_bytes": 48092, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-200000Z_60.png", + "type": "file", + "size_bytes": 45757, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-210000Z_60.png", + "type": "file", + "size_bytes": 41888, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-220000Z_60.png", + "type": "file", + "size_bytes": 43857, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101012-230000Z_60.png", + "type": "file", + "size_bytes": 45503, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-000000Z_60.png", + "type": "file", + "size_bytes": 40867, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-010000Z_60.png", + "type": "file", + "size_bytes": 41826, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-020000Z_60.png", + "type": "file", + "size_bytes": 44875, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-030000Z_60.png", + "type": "file", + "size_bytes": 42020, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-040000Z_60.png", + "type": "file", + "size_bytes": 42820, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-050000Z_60.png", + "type": "file", + "size_bytes": 38260, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-060000Z_60.png", + "type": "file", + "size_bytes": 41557, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-070000Z_60.png", + "type": "file", + "size_bytes": 42659, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-080000Z_60.png", + "type": "file", + "size_bytes": 34909, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-090000Z_60.png", + "type": "file", + "size_bytes": 31175, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-100000Z_60.png", + "type": "file", + "size_bytes": 29885, + "mime": "image/png" + }, + { + "path": "/1234/Towsey.Acoustic/ZoomingTiles", + "name": "ZoomingTiles", + "type": "directory", + "has_children": true + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-110000Z_60.png", + "type": "file", + "size_bytes": 29891, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-120000Z_60.png", + "type": "file", + "size_bytes": 29532, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__ACI-ENT-EVN.Tile_20101013-130000Z_60.png", + "type": "file", + "size_bytes": 24295, + "mime": "image/png" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__Towsey.Acoustic.Indices.csv", + "type": "file", + "size_bytes": 452247, + "mime": "text/csv" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__Towsey.Acoustic.Indices_BACKUP.csv", + "type": "file", + "size_bytes": 452247, + "mime": "text/csv" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__Towsey.Acoustic.ACI.csv", + "type": "file", + "size_bytes": 6581066, + "mime": "text/csv" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__Towsey.Acoustic.BGN.csv", + "type": "file", + "size_bytes": 6581465, + "mime": "text/csv" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__Towsey.Acoustic.CVR.csv", + "type": "file", + "size_bytes": 6821263, + "mime": "text/csv" + }, + { + + "name": "4c77b524-1857-4550-afaa-c0ebe5e3960a_20101012-140000Z__Towsey.Acoustic.DIF.csv", + "type": "file", + "size_bytes": 6213756, + "mime": "text/csv" + } + ] + } + ]; + fakedData = casingTransformers.transformObject(fakedData, casingTransformers.camelize); + + function query() { + //const path = paths.api.routes.analysisResults; + return $q.when({data: {data: fakedData}}) + .then(x => AnalysisResultModel.makeFromApi(x)); + } + + function get(path, page = 1) { + + let data = angular.copy({data: fakedData.find(x => x.path === path), meta:{}}); + let length = (data.data.children || []).length; + if (length > 100) { + let offset = (page - 1) * 100; + data.meta = { + paging: { + "page": page, + "items": 100, + "total": length, + "maxPage": Math.ceil(length / 100), + "current": null, + "previous": null, + "next": null + } + }; + data.data.children = data.data.children.slice(offset, offset + 100); + } + + return $q.when({data}) + .then(x => AnalysisResultModel.makeFromApi(x)); + } + + return { + query, + get + }; + } + ] + ); diff --git a/src/components/services/audioEvent.js b/src/components/services/audioEvent.js index eac8dc8d..af2e2098 100644 --- a/src/components/services/audioEvent.js +++ b/src/components/services/audioEvent.js @@ -77,7 +77,7 @@ angular return q; }); - return $http.post(url, qb.toJSON()); + return $http.post(url, qb.toJSONString()); }; const filterUrl = paths.api.routes.audioEvent.filterAbsolute; @@ -87,7 +87,7 @@ angular }); return $http - .post(filterUrl, query.toJSON()) + .post(filterUrl, query.toJSONString()) .then(x => AudioEventModel.makeFromApi(x)); }; @@ -100,7 +100,7 @@ angular ); }); - return $http.post(filterUrl, query.toJSON()) + return $http.post(filterUrl, query.toJSONString()) .then(resultPager.loadAll) .then(x => AudioEventModel.makeFromApi(x)); }; diff --git a/src/components/services/audioRecording.js b/src/components/services/audioRecording.js index 072c95c5..153f0fc6 100644 --- a/src/components/services/audioRecording.js +++ b/src/components/services/audioRecording.js @@ -30,7 +30,7 @@ angular resource.getRecentRecordings = function () { - return $http.post(filterUrl, query.toJSON()); + return $http.post(filterUrl, query.toJSONString()); }; resource.getRecordingsForVisualization = function (siteIds) { @@ -45,7 +45,7 @@ angular }); return $http - .post(filterUrl, query.toJSON()) + .post(filterUrl, query.toJSONString()) .then(x => AudioRecordingCore.makeFromApi(x)); }; @@ -55,7 +55,7 @@ angular .project({include: ["id", "siteId", "durationSeconds", "recordedDate"]})); return $http - .post(filterUrl, query.toJSON()) + .post(filterUrl, query.toJSONString()) .then(x => AudioRecordingModel.makeFromApi(x)); }; return resource; diff --git a/src/components/services/bawResource.spec.js b/src/components/services/bawResource.spec.js index ee59e88f..48cdb255 100644 --- a/src/components/services/bawResource.spec.js +++ b/src/components/services/bawResource.spec.js @@ -14,17 +14,17 @@ describe("The bawResource service", function () { $rootScope = _$rootScope; }])); - - it("should return a resource constructor that includes update/put", function () { - - expect(bawResource("/test")).toImplement({ - "get": null, - "save": null, - "query": null, - "remove": null, - "delete": null, - "update": null, - "modifiedPath": null + // jasmineMatchers' toImplement currently does not support testing for fields on Function objects + xit("should return a resource constructor that includes update/put", function () { + var resource = bawResource("/test"); + expect(resource).toImplement({ + "get": Function, + "save": Function, + "query": Function, + "remove": Function, + "delete": Function, + "update": Function, + "modifiedPath": String }); }); diff --git a/src/components/services/bookmark.spec.js b/src/components/services/bookmark.spec.js index c4e99b17..345ddb97 100644 --- a/src/components/services/bookmark.spec.js +++ b/src/components/services/bookmark.spec.js @@ -13,9 +13,9 @@ describe("The bookmark service", function () { it("will return a promise for retrieving application bookmarks", function() { expect(bawResource.applicationBookmarksPromise).toImplement({ - catch: null, - finally: null, - then: null + catch: Function, + finally: Function, + then: Function }); }); }); \ No newline at end of file diff --git a/src/components/services/mime.js b/src/components/services/mime.js new file mode 100644 index 00000000..7e19abde --- /dev/null +++ b/src/components/services/mime.js @@ -0,0 +1,74 @@ +angular + .module("bawApp.services.mime", []) + .service( + "MimeType", + [ + function () { + let make = (name, icon, mimeTypes, extensions) => ({extensions, icon, mimeTypes, name}); + + let mimeTypes = [ + make("png", "fa-file-image-o", ["image/png"], ["png"]), + make("jpeg", "fa-file-image-o", ["image/jpeg"], ["jpeg", "jpg", "jpe", "pjpeg"]), + make("gif", "fa-file-image-o", ["image/gif"], ["gif"]), + make("bitmap", "fa-file-image-o", ["image/bitmap"], ["bmp"]), + make("wave", "fa-file-audio-o", [ + "audio/wav", + "audio/x-wav", + "audio/wave", + "audio/x-pn-wav"], ["wav"]), + make("mp3", "fa-file-audio-o", ["audio/mpeg3", "audio/x-mpeg-3"], ["mp3"]), + make("csv", "fa-file-excel-o", ["text/csv"], ["csv"]), + make("html", "fa-file-code-o", ["text/html", "application/xhtml+xml"], ["html", "xhtml"]), + make("plain", "fa-file-text-o", ["text/plain"], ["txt"]), + make("yaml", "fa-file-code-o", ["application/x-yaml", "text/yaml"], ["yaml", "yml"]), + make("xml", "fa-file-code-o", ["application/x-xml", "application/xml", "text/xml"], ["xml"]), + make("json", "fa-file-code-o", [ + "application/x-json", + "application/json", + "text/json", + "text/x-json"], ["json"]), + make("pdf", "fa-file-pdf-o", ["application/pdf"], ["pdf"]), + make("zip", "fa-file-archive-o", ["application/zip"], ["zip"]), + make("gzip", "fa-file-archive-o", ["application/gzip", "application/x-gzip"], ["gz", "gzip"]), + make("binary", "fa fa-file-o", ["application/octet-stream"], null), + make("unknown", "fa fa-file-o", ["application/unknown"], null) + ]; + + + let reverseLookup = new Map( + mimeTypes.reduce( + (rest, m) => rest.concat( + m.mimeTypes.map(mt => [mt, m]) + ), + [] + ) + ); + + + function mimeToMode(mimeType) { + if (reverseLookup.has(mimeType)) { + return reverseLookup.get(mimeType).name; + } + else { + return null; + } + } + + function mimeToFaIcon(mimeType) { + if (reverseLookup.has(mimeType)) { + return "fa " + reverseLookup.get(mimeType).icon; + } + else { + return "fa fa fa-file-o"; + } + } + + return { + mimeTypes, + mimeToMode, + mimeToFaIcon + }; + + } + ] + ); diff --git a/src/components/services/predictiveCache.spec.js b/src/components/services/predictiveCache.spec.js index 55915729..320d2e78 100644 --- a/src/components/services/predictiveCache.spec.js +++ b/src/components/services/predictiveCache.spec.js @@ -57,8 +57,8 @@ describe("The predictiveCache service", function () { it("ensure the interceptor implements the expected methods", function () { expect(predictiveCacheInterceptor).toImplement({ - response: null, - listeners: null + response: Function, + listeners: Function }); }); diff --git a/src/components/services/project.js b/src/components/services/project.js index 9af6d0a4..61e0da55 100644 --- a/src/components/services/project.js +++ b/src/components/services/project.js @@ -26,13 +26,15 @@ angular {projectId: "@projectId"} ); - var gapUrl = paths.api.routes.project.filterAbsolute; - var gapQuery = QueryBuilder.create(function (q) { - return q.project({"include": ["id", "name"]}); - }); - resource.getAllProjects = function () { + + resource.getAllProjectNames = function () { + const gapUrl = paths.api.routes.project.filterAbsolute; + const gapQuery = QueryBuilder.create(function (q) { + return q.project({"include": ["id", "name"]}); + }); + return $http - .post(gapUrl, gapQuery.toJSON()) + .post(gapUrl, gapQuery.toJSONString()) .then(x => ProjectModel.makeFromApi(x)); }; @@ -47,7 +49,7 @@ angular }); return $http - .post(gpbiUrl, query.toJSON()) + .post(gpbiUrl, query.toJSONString()) .then(x => ProjectModel.makeFromApi(x)); }; @@ -61,7 +63,7 @@ angular }); return $http - .post(gpbsiUrl, query.toJSON()) + .post(gpbsiUrl, query.toJSONString()) .then(x => ProjectModel.makeFromApi(x)); }; diff --git a/src/components/services/queryBuilder.js b/src/components/services/queryBuilder.js index 77ba9233..c911e5ef 100644 --- a/src/components/services/queryBuilder.js +++ b/src/components/services/queryBuilder.js @@ -417,7 +417,7 @@ angular return this.end.call(query); }; - this.toJSON = function toJSON(spaces) { + this.toJSON = function() { var compiledQuery = {}, that = this; @@ -427,7 +427,11 @@ angular } }); - return JSON.stringify(compiledQuery, null, spaces); + return compiledQuery; + }; + + this.toJSONString = function toJSON(spaces) { + return JSON.stringify(this.toJSON(), null, spaces); }; this.toQueryString = function toQueryString() { diff --git a/src/components/services/queryBuilder.spec.js b/src/components/services/queryBuilder.spec.js index 1f9e262e..894589d8 100644 --- a/src/components/services/queryBuilder.spec.js +++ b/src/components/services/queryBuilder.spec.js @@ -56,7 +56,8 @@ describe("The QueryBuilder", function () { }).toThrowError(Error, "The create callback must return a child instance of Query passed to the callback"); }); - it("should implement the expected interface", function () { + // jasmineMatchers' toImplement current does not support testing for fields with null values + xit("should implement the expected interface", function () { var queryInterface = validCombinators.concat(validOperators); var rootInterface = queryInterface.concat(rootOperators); @@ -105,7 +106,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q.or(q.eq("field", 3.0), q.lt("field", 6.0))); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("will gracefully resolve key conflicts for combinators in the deep merge - test 2", function() { @@ -132,7 +133,7 @@ describe("The QueryBuilder", function () { return q; }); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("a query operator should return a new instance of a Query", function () { @@ -169,7 +170,7 @@ describe("The QueryBuilder", function () { it("should be able to produce a bare query", function () { var expected = {}; - expect(q.toJSON(spaces)).toBe(j(expected)); + expect(q.toJSONString(spaces)).toBe(j(expected)); }); @@ -186,7 +187,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q.and(q.eq("field", 3.0))); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should ensure .end and .compose and .create are the same", function () { @@ -205,16 +206,16 @@ describe("The QueryBuilder", function () { }; var actualCompose = q.compose(q.eq("fieldA", 3.0).or(q.field("fieldB").lt(6.0).gt(3.0))); - expect(actualCompose.toJSON(spaces)).toBe(j(expected)); + expect(actualCompose.toJSONString(spaces)).toBe(j(expected)); q = queryBuilder.create(); var actualEnd = q.eq("fieldA", 3.0).or(q.field("fieldB").lt(6.0).gt(3.0)).end(); - expect(actualEnd.toJSON(spaces)).toBe(j(expected)); + expect(actualEnd.toJSONString(spaces)).toBe(j(expected)); var actualCreate = queryBuilder.create(function (q) { return q.eq("fieldA", 3.0).or(q.field("fieldB").lt(6.0).gt(3.0)); }); - expect(actualCreate.toJSON(spaces)).toBe(j(expected)); + expect(actualCreate.toJSONString(spaces)).toBe(j(expected)); }); it("should ensure not is arity:1 only", function () { @@ -234,7 +235,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q.eq("field", 3.0)); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should allow more than one operation at root level", function () { @@ -251,7 +252,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q.eq("fieldA", 3.0).eq("fieldB", 6.0)); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("ensures that the in function only takes an array", function () { @@ -275,7 +276,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q.in("fieldA", new Set([1, 2, 3, 3]))); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("ensures that the in function automatically does a uniqueness check", function () { @@ -289,7 +290,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q.in("fieldA", [1, 2, 3, 3])); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("ensures the regex function is not supported", function () { @@ -315,7 +316,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q[rangeFunction]("fieldA", "(3,20.0)")); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); expected.filter.fieldA[rangeFunction] = { from: 3, @@ -325,7 +326,7 @@ describe("The QueryBuilder", function () { q = queryBuilder.create(); actual = q.compose(q[rangeFunction]("fieldA", {from: 3, to: 20.0})); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it(rangeFunction + " it validates a string range", function () { @@ -399,7 +400,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q[rangeFunction]("fieldA", {from: 20, to: null})); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); expected = { filter: { @@ -414,7 +415,7 @@ describe("The QueryBuilder", function () { q = queryBuilder.create(); actual = q.compose(q[rangeFunction]("fieldA", {from: null, to: 20})); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); expected = { }; @@ -422,7 +423,7 @@ describe("The QueryBuilder", function () { q = queryBuilder.create(); actual = q.compose(q[rangeFunction]("fieldA", {from: null, to: null})); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); }); @@ -436,11 +437,11 @@ describe("The QueryBuilder", function () { }; var actual = q.compose(q.range("fieldA", "(,20.0)")); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); q = queryBuilder.create(); actual = q.compose(q.range("fieldA", {to: 20})); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); expected = { filter: { @@ -452,7 +453,7 @@ describe("The QueryBuilder", function () { q = queryBuilder.create(); actual = q.compose(q.range("fieldA", "(,20.0]")); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("the smart range function simplifies a missing upper bound", function () { @@ -465,7 +466,7 @@ describe("The QueryBuilder", function () { }; var actual = q.compose(q.range("fieldA", "(3,)")); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); expected = { @@ -478,11 +479,11 @@ describe("The QueryBuilder", function () { q = queryBuilder.create(); actual = q.compose(q.range("fieldA", "[3,)")); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); q = queryBuilder.create(); actual = q.compose(q.range("fieldA", {from: 3})); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); }); @@ -508,7 +509,7 @@ describe("The QueryBuilder", function () { var actual = q.compose(q.and( q.lt("fieldA", 3.0).contains("fieldB", "hello").gt("fieldA", 0.0) ).lt("fieldC", 17)); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should handle a more complex query - with separate queries for the same field", function () { @@ -530,7 +531,7 @@ describe("The QueryBuilder", function () { q.lt("fieldA", 3.0).contains("fieldB", "hello"), q.gt("fieldA", 0.0) )); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should handle a more complex query - with lots of nesting", function () { @@ -578,7 +579,7 @@ describe("The QueryBuilder", function () { ) ) )); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should allow paging to be set", function () { @@ -590,7 +591,7 @@ describe("The QueryBuilder", function () { }; var actual = q.page({items: 10, page: 30}); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should validate page arguments", function () { @@ -619,7 +620,7 @@ describe("The QueryBuilder", function () { }; var actual = q.page.disable(); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); @@ -632,7 +633,7 @@ describe("The QueryBuilder", function () { }; var actual = q.page({}).page.disable(); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should should allow re-enabling of paging", function () { @@ -645,7 +646,7 @@ describe("The QueryBuilder", function () { // this essentially represents paging with the default options var actual = q.page.disable().page({}); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); @@ -663,7 +664,7 @@ describe("The QueryBuilder", function () { }; var actual = q.eq("fieldA", 30).page({items: 5, page: 2}).end(); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should always update root with paging...even if on a subquery (for disablePaging too)", function () { @@ -679,7 +680,7 @@ describe("The QueryBuilder", function () { }; var actual = q.eq("fieldA", 30).page.disable().end(); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should allow sorting to be set", function () { @@ -691,7 +692,7 @@ describe("The QueryBuilder", function () { }; var actual = q.sort({orderBy: "durationSeconds", direction: "desc"}); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should validate sorting arguments", function () { @@ -730,7 +731,7 @@ describe("The QueryBuilder", function () { }; var actual = q.lt("fieldB", 6.0).sort({orderBy: "durationSeconds"}).end(); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should allow projection to be set (whitelist)", function () { @@ -743,7 +744,7 @@ describe("The QueryBuilder", function () { }; var actual = q.project({include: ["durationSeconds", "id"]}); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); @@ -756,7 +757,7 @@ describe("The QueryBuilder", function () { } }; var actual = q.project({exclude: ["durationSeconds", "id"]}); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); it("should validate projection arguments", function () { @@ -795,7 +796,7 @@ describe("The QueryBuilder", function () { }; var actual = q.notEq("fieldC", 7.5).project({include: ["durationSeconds", "fieldC"]}).end(); - expect(actual.toJSON(spaces)).toBe(j(expected)); + expect(actual.toJSONString(spaces)).toBe(j(expected)); }); @@ -917,7 +918,7 @@ describe("The QueryBuilder", function () { {page: 1, items: 10} ).end(); - expect(actual.toJSON(spaces)).toBe(j(complexExpected)); + expect(actual.toJSONString(spaces)).toBe(j(complexExpected)); }); it("should be able to load a very complex query", function () { diff --git a/src/components/services/savedSearch.js b/src/components/services/savedSearch.js new file mode 100644 index 00000000..345b93bd --- /dev/null +++ b/src/components/services/savedSearch.js @@ -0,0 +1,58 @@ +angular + .module("bawApp.services.savedSearch", []) + .factory( + "SavedSearch", + [ + "$resource", + "bawResource", + "$http", + "$q", + "conf.paths", + "lodash", + "casingTransformers", + "QueryBuilder", + "baw.models.SavedSearch", + function ($resource, bawResource, $http, $q, paths, _, casingTransformers, QueryBuilder, SavedSearchModel) { + + // FAKED! + let fakedData = [ + + { + "id": 1, + "name": "test saved search - SERF", + "description": "I'm a description and that's ok", + "stored_query":{"siteId":{"in":[398, 401, 399, 402, 400, 508 ] }}, + "creator_id": 9, + "created_at": "2016-02-18T15:21:45.862+10:00", + "project_ids":[397, 469], + "analysis_job_ids":[1] + }, + { + "id": 2, + "name": "FAKE DATA test saved search - SERFishg", + "description": "I'm a description and that's ok ALA LA la La al LAA", + "stored_query":{"siteId":{"in":[398, 754 ] }}, + "creator_id": 144, + "created_at": "2016-03-01T15:21:45.862+10:00", + "project_ids":[397, 645], + "analysis_job_ids":[] + } + ]; + fakedData = casingTransformers.transformObject(fakedData, casingTransformers.camelize); + + function query() { + //const path = paths.api.routes.analysisResults; + return $q.when({data: {data: fakedData}}) + .then(x => SavedSearchModel.makeFromApi(x)); + } + + function get(id) { + return $q.when({data: {data: fakedData.find(x => x.id === id)}}) + .then(x => SavedSearchModel.makeFromApi(x)); + } + + return { + query, + get + }; + }]); diff --git a/src/components/services/scripts.js b/src/components/services/scripts.js new file mode 100644 index 00000000..1bbfe361 --- /dev/null +++ b/src/components/services/scripts.js @@ -0,0 +1,60 @@ +angular + .module("bawApp.services.script", []) + .factory( + "Script", + [ + "$resource", + "bawResource", + "$http", + "$q", + "conf.paths", + "lodash", + "casingTransformers", + "QueryBuilder", + "baw.models.Script", + function ($resource, bawResource, $http, $q, paths, _, casingTransformers, QueryBuilder, ScriptModel) { + + // FAKED! + let fakedData = [ + + { + "id": 1, + "name": "simulate work", + "description": "simulates running an analysis", + "analysis_identifier": "SIMULATE_WORK", + "version": 1, + "creator_id": 1, + "created_at": "2016-02-18T15:58:05.465+10:00", + "executable_settings": `{\n\t"hellllo": "test"\n}`, + "executable_settings_media_type": "application/json" + }, + { + "id": 2, + "name": "simulate work VERSION 2", + "description": "simulates running an analysis", + "analysis_identifier": "SIMULATE_WORK", + "version": 2, + "creator_id": 1, + "created_at": "2016-02-18T15:58:05.465+10:00", + "executable_settings": "---\nAnalysisName: Towsey.KoalaMale\n# min and max of the freq band to search\nMinHz: 250 \nMaxHz: 800\n# duration of DCT in seconds \n# this cannot be too long because the oscillations are not constant.\nDctDuration: 0.30\n# minimum acceptable amplitude of a DCT coefficient\nDctThreshold: 0.5\n# ignore oscillation rates below the min & above the max threshold\n# OSCILLATIONS PER SECOND\nMinOcilFreq: 20 \nMaxOcilFreq: 55\n# Minimum duration for the length of a true event (seconds).\nMinDuration: 0.5\n# Maximum duration for the length of a true event.\nMaxDuration: 2.5\n# Event threshold - Determines FP \/ FN trade-off for events.\nEventThreshold: 0.2\n################################################################################\nSaveIntermediateWavFiles: false\nSaveIntermediateCsvFiles: false\nSaveSonogramImages: false\nDisplayCsvImage: false\nParallelProcessing: false\n#DoNoiseReduction: true\n#BgNoiseThreshold: 3.0\n\nIndexPropertiesConfig: \".\\\\IndexPropertiesConfig.yml\"\n...", + "executable_settings_media_type": "application/x-yaml" + } + ]; + fakedData = casingTransformers.transformObject(fakedData, casingTransformers.camelize); + + function query() { + //const path = paths.api.routes.analysisResults; + return $q.when({data: {data: fakedData}}) + .then(x => ScriptModel.makeFromApi(x)); + } + + function get(id) { + return $q.when({data: {data: fakedData.find(x => x.id === id)}}) + .then(x => ScriptModel.makeFromApi(x)); + } + + return { + query, + get + }; + }]); diff --git a/src/components/services/services.js b/src/components/services/services.js index 18338ec3..5c17017e 100644 --- a/src/components/services/services.js +++ b/src/components/services/services.js @@ -15,6 +15,7 @@ angular.module( "bawApp.services.resultPager", // endpoint specific + "bawApp.services.analysisJob", "bawApp.services.analysisResult", "bawApp.services.analysisResultFile", "bawApp.services.bookmark", @@ -25,7 +26,10 @@ angular.module( "bawApp.services.audioEvent", "bawApp.services.taggings", "bawApp.services.tag", + "bawApp.services.mime", "bawApp.services.media", + "bawApp.services.savedSearch", + "bawApp.services.script", "bawApp.services.birdWalkService", "bawApp.services.breadcrumbs", "bawApp.services.userProfile", diff --git a/src/components/services/site.js b/src/components/services/site.js index 351c3c43..5cb5c0d4 100644 --- a/src/components/services/site.js +++ b/src/components/services/site.js @@ -15,7 +15,7 @@ angular .sort({orderBy: "name"}); }); return $http - .post(url, query.toJSON()) + .post(url, query.toJSONString()) .then( x => SiteModel.makeFromApi(x)); }; @@ -27,7 +27,7 @@ angular .sort({orderBy: "name"}); }); return $http - .post(url, query.toJSON()) + .post(url, query.toJSONString()) .then( x => SiteModel.makeFromApi(x)); }; @@ -38,7 +38,7 @@ angular .sort({orderBy: "name"}); }); return $http - .post(url, query.toJSON()) + .post(url, query.toJSONString()) .then( x => SiteModel.makeFromApi(x)); }; @@ -47,7 +47,7 @@ angular var query = QueryBuilder.create(function (q) { return q.project({"include": ["id", "name"]}); }); - return $http.post(url, query.toJSON()); + return $http.post(url, query.toJSONString()); };*/ diff --git a/src/components/services/tag.js b/src/components/services/tag.js index 640421e9..92602ce8 100644 --- a/src/components/services/tag.js +++ b/src/components/services/tag.js @@ -130,7 +130,7 @@ angular return q.in("audioEvents.id", audioEventIds); }); - return $http.post(url, query.toJSON()).then(x => TagModel.makeFromApi(x)); + return $http.post(url, query.toJSONString()).then(x => TagModel.makeFromApi(x)); }; return resource; diff --git a/src/components/services/unitConverters.spec.js b/src/components/services/unitConverters.spec.js index d7c4e952..68464183 100644 --- a/src/components/services/unitConverters.spec.js +++ b/src/components/services/unitConverters.spec.js @@ -93,13 +93,13 @@ describe("The unitConverter service", function () { it("returns an object that implements the required API", function () { expect(converters).toImplement({ - input: {}, - conversions: {}, - pixelsToSeconds: angular.noop, - secondsToPixels: angular.noop, - hertzToPixels: angular.noop, - invertHertz: angular.noop, - invertPixels: angular.noop + input: Object, + conversions: Object, + pixelsToSeconds: Function, + secondsToPixels: Function, + hertzToPixels: Function, + invertHertz: Function, + invertPixels: Function }); }); diff --git a/src/components/services/url.js b/src/components/services/url.js index fc0ea8a9..38f3682a 100644 --- a/src/components/services/url.js +++ b/src/components/services/url.js @@ -31,34 +31,93 @@ angular if (angular.isUndefined(val) || val === null) { return ""; } + return encodeURIComponent(val). - replace(/%40/gi, "@"). - replace(/%3A/gi, ":"). - replace(/%24/g, "$"). - replace(/%2C/gi, ","). - replace(/%20/g, (pctEncodeSpaces ? "%20" : "+")); + replace(/%40/gi, "@"). + replace(/%3A/gi, ":"). + replace(/%24/g, "$"). + replace(/%2C/gi, ","). + replace(/%3B/gi, ";"). + replace(/%20/g, (pctEncodeSpaces ? "%20" : "+")); } + function toKeyValue(obj, validateKeys, _tokenRenamer) { var tokenRenamer = _tokenRenamer || _renamerFunc; var parts = []; - angular.forEach(obj, function (value, key) { + angular.forEach(obj, function(value, key) { if (validateKeys) { // only add key value pair if value is not undefined, not null, and is not an empty string - var valueIsEmptyString = angular.isString(value) && value.length < 1; - if (angular.isUndefined(value) || value === null || valueIsEmptyString || value === false) { + var valueIsEmptyString = value === ""; + if (value === undefined || value === null || valueIsEmptyString || value === false) { return; } } - var encodedKey = encodeUriQuery(tokenRenamer(key), /* encode spaces */ true); + // apply casing transforms + key = tokenRenamer(key); + + // Angular encodes `true` as just the key without a value - like a flag + if (angular.isArray(value)) { + angular.forEach(value, function(arrayValue) { + parts.push(encodeUriQuery(key, true) + + (arrayValue === true ? "" : "=" + encodeUriQuery(arrayValue, true))); + }); + } else { + parts.push(encodeUriQuery(key, true) + + (value === true ? "" : "=" + encodeUriQuery(value, true))); + } + }); + return parts.length ? parts.join("&") : ""; + } + + /** + * Tries to decode the URI component without throwing an exception. + * + * @private + * @param str value potential URI component to check. + * @returns {boolean} True if `value` can be decoded + * with the decodeURIComponent function. + */ + function tryDecodeURIComponent(value) { + try { + return decodeURIComponent(value); + } catch (e) { + // Ignore any invalid uri component + } + } - // Angular does this: if value is true, just include the key without a value - var encodedValue = value === true ? "" : "=" + encodeUriQuery(value, /* encode spaces */ true); - parts.push(encodedKey + encodedValue); + /** + * Parses an escaped url query string into key-value pairs. + * Lifted from https://github.com/angular/angular.js/blob/0ece2d5e0b34a27baa6238c3c2dcb4f92ccfa805/src/Angular.js#L1289 + * @returns {Object.} + */ + function parseKeyValue(/**string*/keyValue) { + var obj = {}; + angular.forEach((keyValue || "").split("&"), function(keyValue) { + var splitPoint, key, val; + if (keyValue) { + key = keyValue = keyValue.replace(/\+/g,"%20"); + splitPoint = keyValue.indexOf("="); + if (splitPoint !== -1) { + key = keyValue.substring(0, splitPoint); + val = keyValue.substring(splitPoint + 1); + } + key = tryDecodeURIComponent(key); + if (angular.isDefined(key)) { + val = angular.isDefined(val) ? tryDecodeURIComponent(val) : true; + if (!hasOwnProperty.call(obj, key)) { + obj[key] = val; + } else if (angular.isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key],val]; + } + } + } }); - return parts.length ? parts.join("&") : ""; + return obj; } function formatUri(uri, values, tokenRenamer) { @@ -111,7 +170,8 @@ angular encodeUriQuery, toKeyValue, formatUri, - formatUriFast + formatUriFast, + parseKeyValue }; this.registerRenamer = function(suffix, renamerFunc) { diff --git a/src/components/services/userProfile.js b/src/components/services/userProfile.js index 31aa5c39..435db0d4 100644 --- a/src/components/services/userProfile.js +++ b/src/components/services/userProfile.js @@ -57,13 +57,12 @@ angular .then(function success(response) { console.log("User profile loaded"); - exports.profile = (new UserProfileModel(response.data.data, - constants.defaultProfile)); + exports.profile = (new UserProfileModel(response.data.data)); return exports.profile; }, function error(response) { console.error("User profile load failed, default profile loaded", response); - exports.profile = (new UserProfileModel(null, constants.defaultProfile)); + exports.profile = (new UserProfileModel(null)); } ).finally(function () { $rootScope.$broadcast(UserProfileEvents.loaded, exports); @@ -85,7 +84,21 @@ angular .in("id", userIds) .project({"include": ["id", "userName"]}); }); - return $http.post(url, query.toJSON()); + return $http.post(url, query.toJSONString()); + }; + + exports.getUserForMetadataTile = function (userId) { + const url = paths.api.routes.user.filterAbsolute; + // Note: "imageUrls" are included no matter what as of + // v0.18.0 of baw-server. + var query = QueryBuilder.create(function (q) { + return q + .eq("id", userId) + .project({include: ["id", "userName"]}); + }); + + return $http.post(url, query) + .then(x => UserProfileModel.makeFromApi(x)); }; diff --git a/src/components/services/vendorServices/externals.js b/src/components/services/vendorServices/externals.js index c005ff60..111a0bf9 100644 --- a/src/components/services/vendorServices/externals.js +++ b/src/components/services/vendorServices/externals.js @@ -1,13 +1,13 @@ angular .module("bawApp.vendorServices", [ - "bawApp.vendorServices.auto" + "bawApp.vendorServices.auto", //"bawApp.services.core.mySillyLibrary" ]) .config(["humanize-durationProvider", "momentProvider", - "$windowProvider", "d3Provider", - function (humanizeDurationProvider, momentProvider, $windowProvider, d3Provider) { + "$windowProvider", "d3Provider", "c3Provider", + function (humanizeDurationProvider, momentProvider, $windowProvider, d3Provider, c3Provider) { // HACK: add real duration formatting onto moment object! var moment = momentProvider.configureVendorInstance(); @@ -144,4 +144,16 @@ angular return this.attr("clip-path"); } }; + + // augment c3 + var c3 = c3Provider.configureVendorInstance(); + let originalC3Generate = c3.generate; + c3.generate = function(...args) { + window.d3 = d3; + + let result = originalC3Generate.apply(c3.chart.internal, args); + + delete window.d3; + return result; + }; }]); diff --git a/src/index.html b/src/index.html index 8ec9b6d4..f27a67ed 100644 --- a/src/index.html +++ b/src/index.html @@ -16,7 +16,7 @@ + src="https://maps.googleapis.com/maps/api/js?key=<%= build_configs.values.keys.googleMaps %>"> <% scripts.forEach( function ( file ) { %> @@ -36,8 +36,8 @@ - {{::brand.name}} + ng-href="{{:: paths.api.links.homeAbsolute }}" target="_self"> + {{:: brand.name }} @@ -46,14 +46,14 @@