├── tests ├── templates │ ├── application.handlebars │ ├── tag.handlebars │ ├── camelParent.handlebars │ ├── camelUrls.handlebars │ ├── transformers.handlebars │ ├── user.handlebars │ ├── preserialized.handlebars │ ├── cart.handlebars │ ├── speakers.handlebars │ ├── ratings.handlebars │ ├── camels.handlebars │ ├── obituaries.handlebars │ ├── new-obituary.handlebars │ ├── associations.handlebars │ ├── speaker.handlebars │ ├── others.handlebars │ ├── sessions.handlebars │ ├── other.handlebars │ └── session.handlebars ├── transforms_tests.js ├── helper.js ├── adapter_embedded_tests.js ├── adapter_polymorphic_tests.js ├── app.js ├── adapter_tests.js └── lib │ ├── jquery.mockjax.js │ └── handlebars-v1.2.1.js ├── .travis.yml ├── grunt ├── karma.js ├── copy.js ├── neuter.js ├── uglify.js ├── jshint.js ├── watch.js ├── bump.js ├── emberhandlebars.js ├── concat.js ├── usebanner.js ├── replace.js └── get_git_rev.js ├── .gitignore ├── src ├── main.js ├── version.js ├── initializer.js ├── transforms │ ├── datetime.js │ └── date.js ├── adapter.js └── serializer.js ├── generators └── license.js ├── karma.conf.js ├── Gruntfile.js ├── bower.json ├── package.json ├── LICENSE ├── .jshintrc ├── CHANGELOG.md └── README.md /tests/templates/application.handlebars: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /tests/templates/tag.handlebars: -------------------------------------------------------------------------------- 1 | {{content.description}} -------------------------------------------------------------------------------- /grunt/karma.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | unit: { 3 | configFile: 'karma.conf.js' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/templates/camelParent.handlebars: -------------------------------------------------------------------------------- 1 |
{{model.name}}
2 | 3 | -------------------------------------------------------------------------------- /tests/templates/camelUrls.handlebars: -------------------------------------------------------------------------------- 1 | {{#each camelUrl in controller}} 2 | {{camelUrl.test}} 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /grunt/copy.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bump: { 3 | expand: true, 4 | cwd: 'dist/', 5 | src: '**', 6 | dest: 'build/' 7 | } 8 | } -------------------------------------------------------------------------------- /tests/templates/transformers.handlebars: -------------------------------------------------------------------------------- 1 | {{#each transformer in controller}} 2 | {{transformer.transformed}} 3 | {{/each}} -------------------------------------------------------------------------------- /grunt/neuter.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | build: { 3 | basePath: 'src/', 4 | src: 'src/main.js', 5 | dest: 'dist/ember-data-django-rest-adapter.js' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/templates/user.handlebars: -------------------------------------------------------------------------------- 1 |
{{model.username}}
2 | {{#each thing in model.aliases}} 3 |
{{thing.name}}
4 | {{/each}} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | dist 4 | tmp 5 | *swp 6 | *.log 7 | 8 | node_modules/* 9 | bower_components/ 10 | tests/dist/ 11 | 12 | tests/lib/tmpl.min.js 13 | -------------------------------------------------------------------------------- /grunt/uglify.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | src: 'dist/ember-data-django-rest-adapter.prod.js', 4 | dest: 'dist/ember-data-django-rest-adapter.min.js' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/templates/preserialized.handlebars: -------------------------------------------------------------------------------- 1 | {{#each record in controller}} 2 | {{#each item in record.config}} 3 |
{{item}}
4 | {{/each}} 5 | {{/each}} 6 | -------------------------------------------------------------------------------- /grunt/jshint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | all: [ 3 | 'dist/ember-data-django-rest-adapter.js', 4 | 'tests/adapter_tests.js', 5 | 'tests/adapter_embedded_tests.js' 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | require("./serializer"); 2 | require("./adapter"); 3 | require('./transforms/date'); 4 | require('./transforms/datetime'); 5 | require("./initializer"); 6 | require("./version"); 7 | -------------------------------------------------------------------------------- /tests/templates/cart.handlebars: -------------------------------------------------------------------------------- 1 | {{input value=name class="name"}} 2 | 3 | {{view Ember.Checkbox checked=complete class="complete"}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /grunt/watch.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | watch: { 3 | options: { 4 | livereload: false, 5 | spawn: false 6 | }, 7 | files: ['src/*.js'], 8 | tasks: ['build'] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/templates/speakers.handlebars: -------------------------------------------------------------------------------- 1 | {{#each speaker in controller}} 2 | {{speaker.name}} 3 | {{#each persona in speaker.personas}} 4 | {{persona.nickname}}
5 | {{/each}} 6 | {{/each}} 7 | -------------------------------------------------------------------------------- /grunt/bump.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | files: ['package.json', 'bower.json'], 4 | updateConfigs: ['package'], 5 | commitFiles: ['package.json', 'bower.json', 'build/'], 6 | push: false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/templates/ratings.handlebars: -------------------------------------------------------------------------------- 1 | 2 | {{#each rating in controller}} 3 | 4 | 5 | 6 | 7 | {{/each}} 8 |
{{rating.score}}
9 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | var VERSION = "DJANGO-REST-ADAPTER-VERSION"; 2 | 3 | DS.DjangoRESTSerializer.VERSION = VERSION; 4 | DS.DjangoRESTAdapter.VERSION = VERSION; 5 | 6 | if (Ember.libraries) { 7 | Ember.libraries.register("ember-data-django-rest-adapter", VERSION); 8 | } 9 | -------------------------------------------------------------------------------- /tests/templates/camels.handlebars: -------------------------------------------------------------------------------- 1 | {{#each camel in controller}} 2 | {{camel.camelCaseAttribute}} 3 | {{#each tag in camel.camelCaseRelationship}} 4 | {{tag.description}} 2 |
  • Hi
  • 3 | {{#each}} 4 |
  • 5 |

    {{publishOnUtc}}

    6 |

    {{timeOfDeathUtc}}

    7 |
  • 8 | {{/each}} 9 | 10 | -------------------------------------------------------------------------------- /src/initializer.js: -------------------------------------------------------------------------------- 1 | Ember.Application.initializer({ 2 | name: 'DjangoDatetimeTransforms', 3 | 4 | initialize: function(container, application) { 5 | application.register('transform:date', DS.DjangoDateTransform); 6 | application.register('transform:datetime', DS.DjangoDatetimeTransform); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /generators/license.js: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Project: Ember Data Django Rest Adapter 3 | // Copyright: (c) 2013 Toran Billups http://toranbillups.com 4 | // License: MIT 5 | // ========================================================================== 6 | 7 | -------------------------------------------------------------------------------- /grunt/emberhandlebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compile: { 3 | options: { 4 | templateName: function (sourceFile) { 5 | var newSource = sourceFile.replace('tests/templates/', ''); 6 | return newSource.replace('.handlebars', ''); 7 | } 8 | }, 9 | files: ['tests/templates/*.handlebars'], 10 | dest: 'tests/lib/tmpl.min.js' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /grunt/concat.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: { 3 | src: [ 4 | 'tests/lib/jquery-1.9.1.js', 5 | 'tests/lib/handlebars-v1.2.1.js', 6 | 'tests/lib/ember.js', 7 | 'tests/lib/ember-data.js', 8 | 'tests/lib/jquery.mockjax.js', 9 | 'tests/lib/tmpl.min.js', 10 | 'dist/ember-data-django-rest-adapter.js', 11 | 'tests/app.js' 12 | ], 13 | dest: 'tests/dist/deps.min.js' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/templates/new-obituary.handlebars: -------------------------------------------------------------------------------- 1 |

    Add an Obituary

    2 | 3 |
    4 |
    5 | 6 | {{input class='publish-on' value=publishOn}} 7 |
    8 |
    9 | 10 | {{input class='time-of-death' value=timeOfDeath}} 11 |
    12 |
    13 | 14 |
    15 |
    16 | -------------------------------------------------------------------------------- /tests/templates/associations.handlebars: -------------------------------------------------------------------------------- 1 | {{#each association in controller}} 2 | Association: {{association.name}}
    3 | {{#each speaker in association.speakers}} 4 | Speaker: {{speaker.name}}
    5 | {{#each persona in speaker.personas}} 6 | Persona: {{persona.nickname}}
    7 | Company: {{persona.company.name}}
    8 | {{#each sponsor in persona.company.sponsors}} 9 | Sponsor: {{sponsor.name}}
    10 | {{/each}} 11 | {{/each}} 12 | {{/each}} 13 | {{/each}} 14 | 15 | -------------------------------------------------------------------------------- /tests/templates/speaker.handlebars: -------------------------------------------------------------------------------- 1 | {{input class="name" value=name}}
    2 |
    {{#each errors.name}}{{this}}{{/each}}

    3 | {{input class="location" value=location}}
    4 |
    5 | 6 | {{#each persona in model.personas}} 7 | {{persona.nickname}}
    8 | {{/each}} 9 | 10 | {{#each badge in model.badges}} 11 | {{badge.city}}
    12 | {{/each}} 13 | -------------------------------------------------------------------------------- /tests/templates/others.handlebars: -------------------------------------------------------------------------------- 1 | 2 | {{#each other in controller}} 3 | 4 | 5 | 6 | 7 | {{#each speaker in other.speakers}} 8 | 11 | {{/each}} 12 | 13 | 14 | {{#each tag in other.tags}} 15 | 18 | {{/each}} 19 | 20 | 23 | 24 | {{/each}} 25 |
    {{other.hat}}
    9 | {{speaker.name}} 10 |
    16 | {{tag.description}}
    17 |
    21 | {{#link-to 'other' other}}View Other Details{{/link-to}} 22 |
    26 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: 'tests', 4 | frameworks: ['qunit'], 5 | files: [ 6 | 'dist/deps.min.js', 7 | 'helper.js', 8 | 'adapter_tests.js', 9 | 'adapter_embedded_tests.js', 10 | 'adapter_polymorphic_tests.js', 11 | 'transforms_tests.js' 12 | ], 13 | reporters: ['dots'], 14 | port: 9876, 15 | colors: true, 16 | logLevel: config.LOG_ERROR, 17 | autoWatch: false, 18 | browsers: ['PhantomJS'], 19 | singleRun: true 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | require('load-grunt-config')(grunt); 4 | 5 | grunt.task.registerTask('release', ['bump-only', 'dist', 'usebanner:bump', 'copy:bump', 'bump-commit']); 6 | grunt.task.registerTask('test', ['dist', 'usebanner:distBanner', 'jshint', 'emberhandlebars', 'concat:test', 'karma']); 7 | grunt.task.registerTask('build', ['neuter:build', 'replace:update_version']); 8 | grunt.task.registerTask('dist', ['build', 'replace:strip_debug_messages_production', 'uglify:dist', 'get_git_rev']); 9 | grunt.task.registerTask('default', ['dist', 'usebanner:distBanner']); 10 | } 11 | -------------------------------------------------------------------------------- /grunt/usebanner.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | distBanner: { 3 | options: { 4 | position: 'top', 5 | banner: '<%= grunt.file.read("generators/license.js") %>\n<%= grunt.gitRevTags %><%= grunt.gitRevSha %>\n', 6 | linebreak: true 7 | }, 8 | files: { 9 | src: ['dist/*.js'] 10 | } 11 | }, 12 | bump: { 13 | options: { 14 | position: 'top', 15 | banner: '<%= grunt.file.read("generators/license.js") %>\n// v<%= package.version %>\n<%= grunt.gitRevSha %>\n', 16 | linebreak: true 17 | }, 18 | files: { 19 | src: ['dist/*.js'] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/transforms/datetime.js: -------------------------------------------------------------------------------- 1 | DS.DjangoDatetimeTransform = DS.Transform.extend({ 2 | deserialize: function(serialized) { 3 | if (typeof serialized === 'string') { 4 | return new Date(Ember.Date.parse(serialized)); 5 | } else if (typeof serialized === 'number') { 6 | return new Date(serialized); 7 | } else if (Ember.isEmpty(serialized)) { 8 | return serialized; 9 | } else { 10 | return null; 11 | } 12 | }, 13 | serialize: function(datetime) { 14 | if (datetime instanceof Date && datetime.toString() !== 'Invalid Date') { 15 | return datetime.toJSON(); 16 | } else { 17 | return null; 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /grunt/replace.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | update_version: { 3 | src: 'dist/ember-data-django-rest-adapter.js', 4 | overwrite: true, 5 | replacements: [{ 6 | from: 'DJANGO-REST-ADAPTER-VERSION', 7 | to: '<%= package.version %>' 8 | }] 9 | }, 10 | // modeled after https://github.com/emberjs/ember-dev/blob/master/lib/ember-dev/rakep/filters.rb#L6 11 | // for some reason the start '^' and end of line '$' do not work in these regexes 12 | strip_debug_messages_production: { 13 | src: 'dist/ember-data-django-rest-adapter.js', 14 | dest: 'dist/ember-data-django-rest-adapter.prod.js', 15 | replacements: [{ 16 | from: /Ember.(assert|deprecate|warn|debug)\(.*\)/g, 17 | to: '' 18 | }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-data-django-rest-adapter", 3 | "version": "1.0.6", 4 | "homepage": "https://github.com/toranb/ember-data-django-rest-adapter", 5 | "authors": [ 6 | "Toran Billups " 7 | ], 8 | "description": "An ember-data adapter for django web applications powered by the django-rest-framework", 9 | "main": "build/ember-data-django-rest-adapter.js", 10 | "dependencies": { 11 | "ember": ">=1.2 <2", 12 | "ember-data": "~1.0.0-beta.7" 13 | }, 14 | "keywords": [ 15 | "ember", 16 | "data", 17 | "django", 18 | "rest", 19 | "adapter" 20 | ], 21 | "license": "MIT", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.6", 3 | "dependencies": { 4 | "grunt": "*", 5 | "grunt-karma": "~0.9.0", 6 | "grunt-cli": "*", 7 | "grunt-contrib-concat": "*", 8 | "grunt-contrib-jshint": "*", 9 | "grunt-ember-template-compiler": "1.3.0", 10 | "karma-qunit": "*", 11 | "karma-qunit-special-blend": "*", 12 | "karma": "~0.12", 13 | "load-grunt-config": "~0.7" 14 | }, 15 | "scripts": { 16 | "test": "grunt test" 17 | }, 18 | "devDependencies": { 19 | "grunt-banner": "*", 20 | "grunt-bump": "0.0.13", 21 | "grunt-contrib-copy": "~0.5.0", 22 | "grunt-contrib-uglify": "~0.2.4", 23 | "grunt-contrib-watch": "~0.5.3", 24 | "grunt-neuter": "~0.6.0", 25 | "grunt-text-replace": "~0.3.8", 26 | "karma-phantomjs-launcher": "^0.1.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/transforms/date.js: -------------------------------------------------------------------------------- 1 | DS.DjangoDateTransform = DS.Transform.extend({ 2 | deserialize: function(serialized) { 3 | if (typeof serialized === 'string') { 4 | return new Date(Ember.Date.parse(serialized)); 5 | } else if (typeof serialized === 'number') { 6 | return new Date(serialized); 7 | } else if (Ember.isEmpty(serialized)) { 8 | return serialized; 9 | } else { 10 | return null; 11 | } 12 | }, 13 | serialize: function(date) { 14 | if (date instanceof Date && date.toString() !== 'Invalid Date') { 15 | year = date.getFullYear(); 16 | month = date.getMonth() + 1; // getMonth is 0-indexed 17 | month = month < 10 ? '0' + month : month; 18 | day = date.getDate(); 19 | return year + '-' + month + '-' + day; 20 | } else { 21 | return null; 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /tests/templates/sessions.handlebars: -------------------------------------------------------------------------------- 1 | 2 | {{#each session in controller}} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{#each speaker in session.speakers}} 11 | 12 | {{/each}} 13 | 14 | 15 | {{#each rating in session.ratings}} 16 | 17 | {{/each}} 18 | 19 | 20 | {{#each tag in session.tags}} 21 | 22 | {{/each}} 23 | 24 | 25 | 26 | 27 | {{/each}} 28 |
    {{session.name}}
    {{session.room}}
    {{speaker.name}}
    {{rating.score}}
    {{tag.description}}
    {{#link-to 'session' session}}View Session Details{{/link-to}}
    29 | 30 |
    31 | {{#link-to 'associations'}}View All Associations{{/link-to}} 32 |
    33 | 34 |
    35 | {{#link-to 'speakers'}}View Details About Joel{{/link-to}} 36 |
    37 | -------------------------------------------------------------------------------- /tests/templates/other.handlebars: -------------------------------------------------------------------------------- 1 |
    {{model.hat}}
    2 |
    {{model.location.name}}
    3 | 4 |
    5 | {{#each speaker in model.speakers}} 6 | {{speaker.name}} 7 | {{#link-to 'speaker' speaker}}View Speaker Details{{/link-to}} 8 | {{/each}} 9 |
    10 | 11 |
    12 | {{#each tag in model.tags}} 13 | {{tag.description}} 14 | {{/each}} 15 |
    16 | 17 |
    18 | {{#each rating in model.ratings}} 19 | {{rating.score}} 20 | 21 | 22 | {{/each}} 23 |
    24 | 25 |
    26 | {{input class='score' placeholder='score' value=score}} 27 | {{input class='feedback' placeholder='feedback' value=feedback}} 28 | 29 |
    30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Toran Billups http://toranbillups.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /grunt/get_git_rev.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | // stolen from https://github.com/ebryn/ember-model/blob/d44cd01aa900d2e18d0a4d695d0e847821ca0142/tasks/banner.js 3 | grunt.registerTask('get_git_rev', 'Computate the git revision string', function () { 4 | var done = this.async(), 5 | task = this, 6 | exec = require('child_process').exec; 7 | exec('git describe --tags', 8 | function (tags_error, tags_stdout, tags_stderr) { 9 | var tags = tags_stdout; 10 | exec('git log -n 1 --format="%h (%ci)"', 11 | function (sha_error, sha_stdout, sha_stderr) { 12 | var sha = sha_stdout, 13 | gitRevTags = '', 14 | gitRevSha = ''; 15 | 16 | if (!tags_error) { 17 | gitRevTags = "// " + tags; 18 | } 19 | 20 | if (!sha_error) { 21 | gitRevSha = "// " + sha; 22 | } 23 | 24 | // mega hax 25 | grunt.gitRevTags = gitRevTags; 26 | grunt.gitRevSha = gitRevSha; 27 | done(); 28 | }); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "console", 4 | "requireModule", 5 | "Ember", 6 | "DS", 7 | "Handlebars", 8 | "Metamorph", 9 | "ember_assert", 10 | "ember_warn", 11 | "ember_deprecate", 12 | "ember_deprecateFunc", 13 | "require", 14 | "equal", 15 | "notEqual", 16 | "asyncTest", 17 | "test", 18 | "raises", 19 | "deepEqual", 20 | "start", 21 | "stop", 22 | "ok", 23 | "strictEqual", 24 | "module", 25 | "expect", 26 | "minispade", 27 | "async", 28 | "invokeAsync", 29 | "jQuery" 30 | ], 31 | 32 | "node" : false, 33 | "es5" : true, 34 | "browser" : true, 35 | 36 | "boss" : true, 37 | "curly": false, 38 | "debug": false, 39 | "devel": false, 40 | "eqeqeq": true, 41 | "evil": true, 42 | "forin": false, 43 | "immed": false, 44 | "laxbreak": false, 45 | "newcap": true, 46 | "noarg": true, 47 | "noempty": false, 48 | "nonew": false, 49 | "nomen": false, 50 | "onevar": false, 51 | "plusplus": false, 52 | "regexp": false, 53 | "undef": true, 54 | "sub": true, 55 | "strict": false, 56 | "white": false, 57 | "eqnull": true 58 | } -------------------------------------------------------------------------------- /tests/templates/session.handlebars: -------------------------------------------------------------------------------- 1 |
    {{model.name}}
    2 |
    {{model.room}}
    3 | 4 |
    5 | {{#each speaker in model.speakers}} 6 | {{speaker.name}} 7 | {{#link-to 'speaker' speaker}}View Speaker Details{{/link-to}} 8 | {{/each}} 9 |
    10 | 11 |
    12 | {{#each rating in model.ratings}} 13 | {{rating.score}} 14 | 15 |
    16 | {{/each}} 17 |
    18 | 19 |
    20 | {{input class='score' placeholder='score' value=score}} 21 | {{input class='feedback' placeholder='feedback' value=feedback}} 22 | 23 |
    24 | 25 |
    26 | {{input class='speaker_name' placeholder='speaker name' value=speaker}} 27 | {{input class='speaker_location' placeholder='speaker location' value=location}} 28 | 29 | 30 | 31 |
    32 | -------------------------------------------------------------------------------- /tests/transforms_tests.js: -------------------------------------------------------------------------------- 1 | module('transforms integration tests', { 2 | setup: function() { 3 | ajaxHash = null; 4 | App.reset(); 5 | }, 6 | teardown: function() { 7 | $.mockjaxClear(); 8 | } 9 | }); 10 | 11 | test('date attribute serializes properly', function() { 12 | stubEndpointForHttpRequest('/api/new-obituary', {}, 'POST', 201); 13 | visit('/new-obituary'); 14 | fillIn('.publish-on', '2012/08/29'); 15 | click('button.submit'); 16 | 17 | andThen(function() { 18 | equal( 19 | ajaxHash.data, 20 | '{"publish_on":"2012-08-29","time_of_death":null}' 21 | ); 22 | }); 23 | }); 24 | 25 | test('datetime attribute serializes properly', function() { 26 | stubEndpointForHttpRequest('/api/new-obituary', {}, 'POST', 201); 27 | visit('/new-obituary'); 28 | fillIn('.time-of-death', '2014-11-19T17:38:00.000Z'); 29 | click('button.submit'); 30 | 31 | andThen(function() { 32 | equal( 33 | ajaxHash.data, 34 | '{"publish_on":null,"time_of_death":"2014-11-19T17:38:00.000Z"}' 35 | ); 36 | }); 37 | }); 38 | 39 | test('date attribute deserializes properly', function() { 40 | var response = '[{"id":1,"publish_on":"2012-08-29","time_of_death":null}]'; 41 | stubEndpointForHttpRequest('/api/obituaries/', response, 'GET', 200); 42 | visit('/obituaries/'); 43 | 44 | andThen(function() { 45 | equal(find('.publish-on').text(), 'Wed, 29 Aug 2012 00:00:00 GMT'); 46 | }); 47 | }); 48 | 49 | test('datetime attribute deserializes properly', function() { 50 | var response = '[{"id":1,"publish_on":null,"time_of_death":"2014-11-19T17:38:00.000Z"}]'; 51 | stubEndpointForHttpRequest('/api/obituaries/', response, 'GET', 200); 52 | visit('/obituaries/'); 53 | 54 | andThen(function() { 55 | equal(find('.time-of-death').text(), 'Wed, 19 Nov 2014 17:38:00 GMT'); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ember-data-django-rest-adapter Changelog 2 | ======================================== 3 | 4 | * [BUGFIX] Avoid date serializer discrepancies due to local time zone 5 | ([#102](https://github.com/toranb/ember-data-django-rest-adapter/pull/102)) 6 | 7 | 8 | 1.0.6 9 | ----- 10 | * [ENHANCEMENT] Move documentation to GitHub Wiki 11 | ([#90](https://github.com/toranb/ember-data-django-rest-adapter/issues/90)) 12 | * [ENHANCEMENT] Add date and datetime transforms 13 | ([#96](https://github.com/toranb/ember-data-django-rest-adapter/pull/96)) 14 | * [ENHANCEMENT] Enable PATCH requests 15 | ([#97](https://github.com/toranb/ember-data-django-rest-adapter/pull/97)) 16 | * [ENHANCEMENT] Enable read-only model attributes 17 | ([#97](https://github.com/toranb/ember-data-django-rest-adapter/pull/97)) 18 | 19 | 20 | 1.0.5 21 | ----- 22 | 23 | * [ENHANCEMENT] Propagate server errors to models 24 | ([#88](https://github.com/toranb/ember-data-django-rest-adapter/pull/88)) 25 | * [BUGFIX] Support for "hasMany" custom embedded keys 26 | ([#86](https://github.com/toranb/ember-data-django-rest-adapter/pull/86)) 27 | * [ENHANCEMENT] Add installation instructions for ember-cli 28 | ([#83](https://github.com/toranb/ember-data-django-rest-adapter/pull/83)) 29 | * [ENHANCEMENT] Add support for polymorphism 30 | ([#85](https://github.com/toranb/ember-data-django-rest-adapter/pull/85)) 31 | 32 | 33 | 1.0.3 34 | ----- 35 | 36 | * [ENHANCEMENT] Update ember and ember-data dependencies 37 | ([cbd75103](https://github.com/toranb/ember-data-django-rest-adapter/commit/cbd7510349594ebc4163408991c09cf98addfe8d)) 38 | 39 | 40 | 1.0.2 41 | ----- 42 | 43 | * [ENHANCEMENT] Disable ember minor version lock 44 | ([#79](https://github.com/toranb/ember-data-django-rest-adapter/pull/79)) 45 | * [BUGFIX] Lock grunt-karma version 46 | ([6ace5940](https://github.com/toranb/ember-data-django-rest-adapter/commit/6ace594018629f26b0fb7c0f914e1711a81f4524)) 47 | * [ENHANCEMENT] Add tests for booleans created from a checkbox 48 | ([0dd73441](https://github.com/toranb/ember-data-django-rest-adapter/commit/0dd73441152ec4dd69b7ab7e26278caeea1c4abe)) 49 | -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | document.write('
    '); 2 | 3 | App.rootElement = '#ember-testing'; 4 | App.setupForTesting(); 5 | App.injectTestHelpers(); 6 | 7 | function exists(selector) { 8 | return !!find(selector).length; 9 | } 10 | 11 | function missing(selector) { 12 | var error = "element " + selector + " found (should be missing)"; 13 | var element = find(selector).length; 14 | equal(element, 0, error); 15 | } 16 | 17 | var expectUrlTypeHashEqual = function(url, type, hash) { 18 | equal(ajaxUrl, url, "ajaxUrl was instead " + ajaxUrl); 19 | equal(ajaxType, type, "ajaxType was instead " + ajaxType); 20 | //hangs test runner? equal(ajaxHash, type, "ajaxHash was instead " + ajaxHash); 21 | }; 22 | 23 | var expectSpeakerAddedToStore = function(pk, expectedName, expectedLocation) { 24 | Ember.run(App, function(){ 25 | var store = App.__container__.lookup("store:main"); 26 | store.find('speaker', pk).then(function(speaker) { 27 | var name = speaker.get('name'); 28 | equal(name, expectedName, "speaker added with name " + name); 29 | var location = speaker.get('location'); 30 | equal(location, expectedLocation, "speaker added with location " + location); 31 | }); 32 | }); 33 | }; 34 | 35 | var expectRatingAddedToStore = function(pk, expectedScore, expectedFeedback, expectedParent, parentName) { 36 | if (parentName == null) { 37 | parentName = "session"; 38 | } 39 | Ember.run(App, function(){ 40 | var store = App.__container__.lookup("store:main"); 41 | store.find('rating', pk).then(function(rating) { 42 | var primaryKey = rating.get('id'); 43 | equal(primaryKey, pk, "rating added with id " + primaryKey); 44 | var score = rating.get('score'); 45 | equal(score, expectedScore, "rating added with score " + score); 46 | var feedback = rating.get('feedback'); 47 | equal(feedback, expectedFeedback, "rating added with feedback " + feedback); 48 | var parentpk = rating.get(parentName).get('id'); 49 | equal(parentpk, expectedParent, "rating added with parent pk " + parentpk); 50 | }); 51 | }); 52 | }; 53 | 54 | function stubEndpointForHttpRequest(url, json, verb, status) { 55 | if (verb == null) { 56 | verb = "GET"; 57 | } 58 | if (status == null) { 59 | status = 200; 60 | } 61 | $.mockjax({ 62 | type: verb, 63 | url: url, 64 | status: status, 65 | dataType: 'json', 66 | responseText: json 67 | }); 68 | } 69 | 70 | $.mockjaxSettings.logging = false; 71 | $.mockjaxSettings.responseTime = 0; 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### END OF LIFE 2 | 3 | This package has reached EOL and is no longer supported. 4 | 5 | For Ember + Django REST Framework projects, use [Ember CLI][] and 6 | [Ember Django Adapter][]. 7 | 8 | 9 | ------------------------------------------------------------------------------- 10 | 11 | 12 | ember-data-django-rest-adapter 13 | ============================== 14 | 15 | [![Build Status][]](https://travis-ci.org/toranb/ember-data-django-rest-adapter) 16 | 17 | This package enables you to build modern web applications using [Ember.js][] 18 | and [Django REST Framework][]. For detailed information on installing and 19 | using the adapter, see the [wiki documentation]. 20 | 21 | 22 | Community 23 | --------- 24 | 25 | * IRC: #ember-django-adapter on freenode 26 | * Issues: [ember-data-django-rest-adapter/issues][] 27 | 28 | 29 | Installing 30 | ---------- 31 | 32 | The adapter is [packaged separately](https://github.com/dustinfarris/ember-django-adapter) 33 | as an Ember CLI add-on. Installation is very simple: 34 | 35 | ``` 36 | npm i --save-dev ember-django-adapter 37 | ``` 38 | 39 | and set the `API_HOST` and `API_NAMESPACE` configuration variables in 40 | `config/environment.js`, e.g.: 41 | 42 | ```js 43 | if (environment === 'development') { 44 | ENV.APP.API_HOST = 'http://localhost:8000'; 45 | ENV.APP.API_NAMESPACE = 'api'; 46 | } 47 | if (environment === 'production') { 48 | ENV.APP.API_HOST = 'https://api.myproject.com'; 49 | ENV.APP.API_NAMESPACE = 'v2'; 50 | } 51 | ``` 52 | 53 | See the [wiki documentation][] for additional installation instructions, 54 | including how to use the adapter with vanilla ember (without using ember-cli). 55 | 56 | 57 | Pending Issues 58 | -------------- 59 | 60 | * Async belongsTo/hasMany requires a pull-request be merged into ember-data 61 | core ([#63][]) 62 | 63 | * Pagination is not yet supported ([#80][]) 64 | 65 | 66 | Credits 67 | ------- 68 | 69 | I took a large part of this project (including the motivation) from @escalant3 70 | and his [tastypie adapter][]. 71 | 72 | Special thanks to all [contributors][]! 73 | 74 | 75 | License 76 | ------- 77 | 78 | Copyright © 2014 Toran Billups http://toranbillups.com 79 | 80 | Licensed under the MIT License 81 | 82 | 83 | [Build Status]: https://secure.travis-ci.org/toranb/ember-data-django-rest-adapter.png?branch=master 84 | [wiki documentation]: https://github.com/toranb/ember-data-django-rest-adapter/wiki 85 | [ember-data-django-rest-adapter/issues]: https://github.com/toranb/ember-data-django-rest-adapter/issues 86 | [Ember.js]: http://emberjs.com/ 87 | [Django REST Framework]: http://www.django-rest-framework.org/ 88 | [Ember CLI]: https://github.com/stefanpenner/ember-cli 89 | [Ember Django Adapter]: https://github.com/dustinfarris/ember-django-adapter 90 | [version 1.0]: https://github.com/dustinfarris/ember-django-adapter/milestones/Version%201.0 91 | [tastypie adapter]: https://github.com/escalant3/ember-data-tastypie-adapter/ 92 | [contributors]: https://github.com/toranb/ember-data-django-rest-adapter/graphs/contributors 93 | [#61]: https://github.com/toranb/ember-data-django-rest-adapter/issues/61 94 | [#63]: https://github.com/toranb/ember-data-django-rest-adapter/pull/63 95 | [#80]: https://github.com/toranb/ember-data-django-rest-adapter/issues/80 96 | -------------------------------------------------------------------------------- /tests/adapter_embedded_tests.js: -------------------------------------------------------------------------------- 1 | module('embedded integration tests', { 2 | setup: function() { 3 | ajaxUrl = undefined; 4 | ajaxType = undefined; 5 | ajaxHash = undefined; 6 | App.reset(); 7 | }, 8 | teardown: function() { 9 | $.mockjaxClear(); 10 | } 11 | }); 12 | 13 | test('ajax response with array of embedded records renders hasMany correctly', function() { 14 | var json = [{"id": 1, "hat": "zzz", "speakers": [{"id": 1, "name": "first", "other": 1}], "ratings": [{"id": 1, "score": 10, "feedback": "nice", "other": 1}], "tags": [{"id": 1, "description": "done"}], "location": {"id": 1, "name": "US"}}]; 15 | 16 | stubEndpointForHttpRequest('/api/others/', json); 17 | visit("/others").then(function() { 18 | var rows = find("table tr").length; 19 | equal(rows, 4, "table had " + rows + " rows"); 20 | var hat = Ember.$.trim($("table tr:eq(0) td:eq(0)").text()); 21 | var speaker = Ember.$.trim($("table tr:eq(1) td:eq(0)").text()); 22 | var tag = Ember.$.trim($("table tr:eq(2) td:eq(0)").text()); 23 | equal(hat, "zzz", "(other) hat was instead: " + hat); 24 | equal(speaker, "first", "speaker was instead: " + speaker); 25 | equal(tag, "done", "tag was instead: " + tag); 26 | }); 27 | }); 28 | 29 | test('ajax response with no embedded records yields empty table', function() { 30 | stubEndpointForHttpRequest('/api/others/', []); 31 | visit("/others").then(function() { 32 | var rows = find("table tr").length; 33 | equal(rows, 0, "table had " + rows + " rows"); 34 | }); 35 | }); 36 | 37 | test('ajax response with single embedded record renders hasMany correctly', function() { 38 | var json = {"id": 1, "hat": "eee", "speakers": [{"id": 1, "name": "first", "other": 1}], "ratings": [{"id": 1, "score": 10, "feedback": "nice", "other": 1}], "tags": [{"id": 1, "description": "done"}], "location": {"id": 1, "name": "US"}}; 39 | stubEndpointForHttpRequest('/api/others/1/', json); 40 | visit("/other/1").then(function() { 41 | var hat = Ember.$.trim($("div .hat").text()); 42 | equal(hat, "eee", "hat was instead: " + hat); 43 | var speaker = Ember.$.trim($("div .name").text()); 44 | equal(speaker, "first", "speaker was instead: " + speaker); 45 | var tag = Ember.$.trim($("div .description").text()); 46 | equal(tag, "done", "tag was instead: " + tag); 47 | }); 48 | }); 49 | 50 | test('ajax response with single embedded record renders belongsTo correctly', function() { 51 | var json = {"id": 1, "hat": "eee", "speakers": [{"id": 1, "name": "first", "other": 1}], "ratings": [{"id": 1, "score": 10, "feedback": "nice", "other": 1}], "tags": [{"id": 1, "description": "done"}], "location": {"id": 1, "name": "US"}}; 52 | stubEndpointForHttpRequest('/api/others/1/', json); 53 | visit("/other/1").then(function() { 54 | var location = Ember.$.trim($("div .location").text()); 55 | equal(location, "US", "location was instead: " + location); 56 | }); 57 | }); 58 | 59 | test('add rating will do http post and append rating to template', function() { 60 | var json = {"id": 1, "hat": "eee", "speakers": [{"id": 1, "name": "first", "other": 1}], "ratings": [{"id": 1, "score": 10, "feedback": "nice", "other": 1}], "tags": [{"id": 1, "description": "done"}], "location": {"id": 1, "name": "US"}}; 61 | var rating = {"id": 3, "score": 4, "feedback": "def", "other": 1}; 62 | stubEndpointForHttpRequest('/api/others/1/', json); 63 | visit("/other/1").then(function() { 64 | var before = find("div .ratings span.score").length; 65 | equal(before, 1, "initially the table had " + before + " ratings"); 66 | //setup the http post mock $.ajax 67 | //for some reason the 2 lines below are not used or needed? 68 | stubEndpointForHttpRequest('/api/ratings/', rating, 'POST', 201); 69 | fillIn(".score", "4"); 70 | fillIn(".feedback", "def"); 71 | return click(".add_rating"); 72 | }).then(function() { 73 | var after = find("div .ratings span.score").length; 74 | equal(after, 2, "table had " + after + " ratings after create"); 75 | expectUrlTypeHashEqual("/api/ratings/", "POST", rating); 76 | expectRatingAddedToStore(3, 4, 'def', 1, 'other'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/adapter.js: -------------------------------------------------------------------------------- 1 | var get = Ember.get; 2 | 3 | DS.DjangoRESTAdapter = DS.RESTAdapter.extend({ 4 | defaultSerializer: "DS/djangoREST", 5 | 6 | /** 7 | * Overrides the `pathForType` method to build underscored URLs. 8 | * 9 | * Stolen from ActiveModelAdapter 10 | * 11 | * ```js 12 | * this.pathForType("famousPerson"); 13 | * //=> "famous_people" 14 | * ``` 15 | * 16 | * @method pathForType 17 | * @param {String} type 18 | * @returns String 19 | */ 20 | pathForType: function(type) { 21 | var decamelized = Ember.String.decamelize(type); 22 | return Ember.String.pluralize(decamelized); 23 | }, 24 | 25 | createRecord: function(store, type, record) { 26 | var url = this.buildURL(type.typeKey); 27 | var data = store.serializerFor(type.typeKey).serialize(record); 28 | return this.ajax(url, "POST", { data: data }); 29 | }, 30 | 31 | updateRecord: function(store, type, record) { 32 | // Partial updates are expected to be sent as a PATCH 33 | var isPartial = false; 34 | record.eachAttribute(function(key, attribute) { 35 | if(attribute.options.readOnly){ 36 | isPartial = true; 37 | } 38 | }, this); 39 | var method = isPartial ? "PATCH" : "PUT"; 40 | 41 | var data = store.serializerFor(type.typeKey).serialize(record); 42 | var id = get(record, 'id'); //todo find pk (not always id) 43 | return this.ajax(this.buildURL(type.typeKey, id), method, { data: data }); 44 | }, 45 | 46 | findMany: function(store, type, ids, parent) { 47 | var url, endpoint, attribute; 48 | 49 | if (parent) { 50 | attribute = this.getHasManyAttributeName(type, parent, ids); 51 | endpoint = store.serializerFor(type.typeKey).keyForAttribute(attribute); 52 | url = this.buildFindManyUrlWithParent(type, parent, endpoint); 53 | } else { 54 | Ember.assert( 55 | "You need to add belongsTo for type (" + type.typeKey + "). No Parent for this record was found"); 56 | } 57 | 58 | return this.ajax(url, "GET"); 59 | }, 60 | 61 | ajax: function(url, type, hash) { 62 | hash = hash || {}; 63 | hash.cache = false; 64 | 65 | return this._super(url, type, hash); 66 | }, 67 | 68 | ajaxError: function(jqXHR) { 69 | var error = this._super(jqXHR); 70 | 71 | if (jqXHR && jqXHR.status === 400) { 72 | var response = Ember.$.parseJSON(jqXHR.responseText), errors = {}; 73 | 74 | Ember.EnumerableUtils.forEach(Ember.keys(response), function(key) { 75 | errors[Ember.String.camelize(key)] = response[key]; 76 | }); 77 | 78 | return new DS.InvalidError(errors); 79 | } else { 80 | return error; 81 | } 82 | }, 83 | 84 | buildURL: function(type, id) { 85 | var url = this._super(type, id); 86 | 87 | if (url.charAt(url.length -1) !== '/') { 88 | url += '/'; 89 | } 90 | 91 | return url; 92 | }, 93 | 94 | buildFindManyUrlWithParent: function(type, parent, endpoint) { 95 | var root, url, parentValue; 96 | 97 | parentValue = parent.get('id'); //todo find pk (not always id) 98 | root = parent.constructor.typeKey; 99 | url = this.buildURL(root, parentValue); 100 | 101 | return url + endpoint + '/'; 102 | }, 103 | 104 | /** 105 | * Extract the attribute name given the parent record, the ids of the 106 | * referenced model, and the type of the referenced model. 107 | * 108 | * Given the model definition 109 | * 110 | * ```js 111 | * App.User = DS.Model.extend({ 112 | * username: DS.attr('string'), 113 | * aliases: DS.hasMany('speaker', { async: true}), 114 | * favorites: DS.hasMany('speaker', { async: true}) 115 | * }); 116 | * ``` 117 | * 118 | * with a model object 119 | * 120 | * ```js 121 | * var user1 = { 122 | * id: 1, 123 | * name: 'name', 124 | * aliases: [2,3], 125 | * favorites: [4,5] 126 | * }; 127 | * 128 | * var type = App.Speaker; 129 | * var parent = user1; 130 | * var ids = [4,5]; 131 | * var name = getHasManyAttributeName(type, parent, ids) // name === "favorites" 132 | * ``` 133 | * 134 | * @method getHasManyAttributeName 135 | * @param {subclass of DS.Model} type 136 | * @param {DS.Model} parent 137 | * @param {Array} ids 138 | * @returns String 139 | */ 140 | getHasManyAttributeName: function(type, parent, ids) { 141 | var attributeName; 142 | parent.eachRelationship(function(name, relationship){ 143 | var relationshipIds; 144 | if (relationship.kind === "hasMany" && relationship.type.typeKey === type.typeKey) { 145 | relationshipIds = parent._data[name].mapBy('id'); 146 | // check if all of the requested ids are covered by this attribute 147 | if (Ember.EnumerableUtils.intersection(ids, relationshipIds).length === ids.length) { 148 | attributeName = name; 149 | } 150 | } 151 | }); 152 | return attributeName; 153 | } 154 | }); 155 | -------------------------------------------------------------------------------- /tests/adapter_polymorphic_tests.js: -------------------------------------------------------------------------------- 1 | module('polymorphic integration tests', { 2 | setup: function() { 3 | // TODO figure out why this isn't getting called 4 | ajaxUrl = undefined; 5 | ajaxType = undefined; 6 | ajaxHash = undefined; 7 | App.reset(); 8 | }, 9 | teardown: function() { 10 | $.mockjaxClear(); 11 | 12 | DS.DjangoRESTSerializer.reopen({ 13 | keyForType: function(key) { 14 | return key + "_type"; 15 | }, 16 | keyForEmbeddedType: function(key) { 17 | return 'type'; 18 | } 19 | }); 20 | } 21 | }); 22 | 23 | asyncTest('test polymorphic hasMany', function() { 24 | var json = { 25 | "id": 100, 26 | "username": "Paul", 27 | "messages": [ 28 | { 29 | "id": 101, 30 | "type": "post", 31 | }, 32 | { 33 | "id": 102, 34 | "type": "comment", 35 | } 36 | ] 37 | }; 38 | 39 | stubEndpointForHttpRequest('/api/users/100/', json); 40 | 41 | Ember.run(App, function(){ 42 | var store = App.__container__.lookup('store:main'); 43 | 44 | store.find('user', 100).then(function(user) { 45 | var messages = user.get('messages').toArray(); 46 | equal(messages.length, 2); 47 | start(); 48 | }); 49 | }); 50 | }); 51 | 52 | asyncTest('test async polymorphic hasMany', function() { 53 | App.reset(); 54 | 55 | App.User.reopen({ 56 | messages: DS.hasMany('message', { polymorphic: true, async: true }) 57 | }); 58 | 59 | var json = { 60 | "id": 200, 61 | "username": "Paul", 62 | "messages": [ 63 | { 64 | "id": 201, 65 | "type": "post", 66 | }, 67 | { 68 | "id": 202, 69 | "type": "comment", 70 | } 71 | ] 72 | }; 73 | 74 | stubEndpointForHttpRequest('/api/users/200/', json); 75 | 76 | Ember.run(App, function(){ 77 | var store = App.__container__.lookup('store:main'); 78 | 79 | store.find('user', 200).then(function(user) { 80 | user.get('messages').then(function(messages) { 81 | equal(messages.toArray().length, 2); 82 | 83 | App.User.reopen({ 84 | messages: DS.hasMany('message', { polymorphic: true }) 85 | }); 86 | 87 | start(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | asyncTest('test polymorphic belongsTo', function() { 94 | App.reset(); 95 | 96 | var message_json = { 97 | "id": 300, 98 | "content": "yo yo yo", 99 | "author": { 100 | "id": 301, 101 | "name": "website", 102 | "type": "company" 103 | } 104 | }; 105 | 106 | stubEndpointForHttpRequest('/api/messages/300/', message_json); 107 | 108 | Ember.run(App, function(){ 109 | var store = App.__container__.lookup('store:main'); 110 | 111 | store.find('message', 300).then(function(message) { 112 | var author = message.get('author'); 113 | 114 | equal(author.get('name'),message_json.author.name); 115 | 116 | start(); 117 | }); 118 | }); 119 | }); 120 | 121 | asyncTest('test async polymorphic belongsTo', function() { 122 | App.reset(); 123 | 124 | App.Message.reopen({ 125 | author: DS.belongsTo('author', { polymorphic: true, async: true }) 126 | }); 127 | 128 | var message_json = { 129 | "id": 400, 130 | "content": "yo yo yo", 131 | "author": 401, 132 | "author_type": "company" 133 | }; 134 | 135 | var company_json = { 136 | "id": 401, 137 | "name": "Big corp" 138 | } 139 | 140 | stubEndpointForHttpRequest('/api/messages/400/', message_json); 141 | stubEndpointForHttpRequest('/api/companies/401/', company_json); 142 | 143 | Ember.run(App, function(){ 144 | var store = App.__container__.lookup('store:main'); 145 | 146 | store.find('message', 400).then(function(message) { 147 | equal(message.get('content'),message_json.content); 148 | 149 | message.get('author').then(function(author) { 150 | equal(author.get('name'),company_json.name); 151 | 152 | App.User.reopen({ 153 | author: DS.belongsTo('author', { polymorphic: true }) 154 | }); 155 | 156 | start(); 157 | }); 158 | 159 | }); 160 | }); 161 | }); 162 | 163 | asyncTest('test loading with custom key for polymorphic belongsTo', function() { 164 | App.reset(); 165 | 166 | DS.DjangoRESTSerializer.reopen({ 167 | keyForEmbeddedType: function(key) { 168 | return 'custom_type'; 169 | } 170 | }); 171 | 172 | var message_json = { 173 | "id": 500, 174 | "content": "yo yo yo", 175 | "receiver": { 176 | "id": 501, 177 | "name": "website", 178 | "custom_type": "company" 179 | } 180 | }; 181 | 182 | var second_message_json = { 183 | "id": 502, 184 | "content": "yo yo yo", 185 | "receiver": { 186 | "id": 501, 187 | "name": "website", 188 | "custom_type": "company" 189 | } 190 | }; 191 | 192 | stubEndpointForHttpRequest('/api/messages/500/', message_json); 193 | stubEndpointForHttpRequest('/api/messages/502/', second_message_json); 194 | 195 | Ember.run(function(){ 196 | var store = App.__container__.lookup('store:main'); 197 | 198 | store.find('message', 500).then(function(message) { 199 | var receiver = message.get('receiver'); 200 | 201 | equal(receiver.get('name'),message_json.receiver.name); 202 | 203 | store.find('message', 502).then(function(message) { 204 | equal(receiver.get('name'),message_json.receiver.name); 205 | 206 | DS.DjangoRESTSerializer.reopen({ 207 | keyForEmbeddedType: function(key) { 208 | return 'type'; 209 | } 210 | }); 211 | 212 | start(); 213 | }); 214 | 215 | }); 216 | }); 217 | }); 218 | 219 | asyncTest('test serializing with custom key for polymorphic belongsTo', function() { 220 | App.reset(); 221 | 222 | DS.DjangoRESTSerializer.reopen({ 223 | keyForType: function(key) { 224 | return key + "_custom_type"; 225 | }, 226 | keyForEmbeddedType: function(key) { 227 | return 'custom_type'; 228 | } 229 | }); 230 | 231 | var message_json = { 232 | "id": 600, 233 | "content": "yo yo yo", 234 | "receiver": { 235 | "id": 601, 236 | "name": "website", 237 | "custom_type": "company" 238 | } 239 | }; 240 | 241 | stubEndpointForHttpRequest('/api/messages/600/', message_json); 242 | 243 | Ember.run(function(){ 244 | var store = App.__container__.lookup('store:main'); 245 | 246 | store.find('message', 600).then(function(message) { 247 | var serialized = store.serialize(message, {includeId: true}); 248 | equal(serialized.receiver, 601); 249 | equal(serialized.receiver_custom_type, 'company'); 250 | 251 | DS.DjangoRESTSerializer.reopen({ 252 | keyForType: function(key) { 253 | return key + "_type"; 254 | }, 255 | keyForEmbeddedType: function(key) { 256 | return 'type'; 257 | } 258 | }); 259 | 260 | start(); 261 | }); 262 | }); 263 | }); 264 | 265 | asyncTest('should not serialize polymorphic hasMany associations', function() { 266 | App.reset(); 267 | 268 | var json = { 269 | "id": 700, 270 | "name": "Paul", 271 | "username": "Paul", 272 | "messages": [ 273 | { 274 | "id": 701, 275 | "type": "post", 276 | } 277 | ] 278 | }; 279 | 280 | stubEndpointForHttpRequest('/api/users/700/', json); 281 | 282 | Ember.run(App, function(){ 283 | var store = App.__container__.lookup('store:main'); 284 | 285 | store.find('user', 700).then(function(user) { 286 | var serialized = store.serialize(user); 287 | 288 | deepEqual(serialized,{name: "Paul", username: "Paul"}); 289 | 290 | start(); 291 | }); 292 | }); 293 | }); 294 | 295 | asyncTest('test custom key for polymorphic hasMany', function() { 296 | DS.DjangoRESTSerializer.reopen({ 297 | keyForEmbeddedType: function(key) { 298 | return 'custom_type'; 299 | } 300 | }); 301 | 302 | App.reset(); 303 | 304 | var json = { 305 | "id": 800, 306 | "username": "Paul", 307 | "messages": [ 308 | { 309 | "id": 801, 310 | "custom_type": "post", 311 | "content": "I am a message" 312 | } 313 | ] 314 | }; 315 | 316 | stubEndpointForHttpRequest('/api/users/800/', json); 317 | 318 | Ember.run(function(){ 319 | var store = App.__container__.lookup('store:main'); 320 | 321 | store.find('user', 800).then(function(user) { 322 | var messages = user.get('messages').toArray(); 323 | var message = messages[0]; 324 | 325 | equal(message.content,json["messages"]["content"]); 326 | 327 | DS.DjangoRESTSerializer.reopen({ 328 | keyForEmbeddedType: function(key) { 329 | return 'type'; 330 | } 331 | }); 332 | 333 | start(); 334 | }); 335 | }); 336 | }); 337 | -------------------------------------------------------------------------------- /src/serializer.js: -------------------------------------------------------------------------------- 1 | var map = Ember.ArrayPolyfills.map; 2 | var forEach = Ember.ArrayPolyfills.forEach; 3 | 4 | DS.DjangoRESTSerializer = DS.RESTSerializer.extend({ 5 | 6 | init: function() { 7 | this._super.apply(this, arguments); 8 | }, 9 | 10 | /** 11 | * @method keyForType 12 | * @param {String} key 13 | * @returns String 14 | */ 15 | keyForType: function(key) { 16 | return key + "_type"; 17 | }, 18 | 19 | /** 20 | * @method keyForEmbeddedType 21 | * @param {String} key 22 | * @returns String 23 | */ 24 | keyForEmbeddedType: function(key) { 25 | return 'type'; 26 | }, 27 | 28 | extractDjangoPayload: function(store, type, payload) { 29 | type.eachRelationship(function(key, relationship){ 30 | var embeddedTypeKey, isPolymorphic = false; 31 | if (relationship.options && relationship.options.polymorphic) { 32 | isPolymorphic = true; 33 | } 34 | 35 | if (!Ember.isNone(payload[key]) && 36 | typeof(payload[key][0]) !== 'number' && 37 | typeof(payload[key][0]) !== 'string' && 38 | relationship.kind ==='hasMany') { 39 | if (Ember.typeOf(payload[key]) === 'array' && payload[key].length > 0) { 40 | if(isPolymorphic) { 41 | // If there is a hasMany polymorphic relationship, push each 42 | // item to the store individually, since they might not all 43 | // be the same type 44 | forEach.call(payload[key],function(hash) { 45 | var type = this.typeForRoot(hash.type); 46 | this.pushSinglePayload(store,type,hash); 47 | }, this); 48 | } else { 49 | var ids = payload[key].mapBy('id'); //todo find pk (not always id) 50 | this.pushArrayPayload(store, relationship.type, payload[key]); 51 | payload[key] = ids; 52 | } 53 | } 54 | } 55 | else if (!Ember.isNone(payload[key]) && typeof(payload[key]) === 'object' && relationship.kind ==='belongsTo') { 56 | var type = relationship.type; 57 | 58 | if(isPolymorphic) { 59 | type = this.typeForRoot(payload[key].type); 60 | } 61 | 62 | var id = payload[key].id; 63 | this.pushSinglePayload(store,type,payload[key]); 64 | 65 | if(!isPolymorphic) payload[key] = id; 66 | } 67 | }, this); 68 | }, 69 | 70 | extractSingle: function(store, type, payload) { 71 | // using normalize from RESTSerializer applies transforms and allows 72 | // us to define keyForAttribute and keyForRelationship to handle 73 | // camelization correctly. 74 | this.normalize(type, payload); 75 | this.extractDjangoPayload(store, type, payload); 76 | return payload; 77 | }, 78 | 79 | extractArray: function(store, type, payload) { 80 | var self = this; 81 | for (var j = 0; j < payload.length; j++) { 82 | // using normalize from RESTSerializer applies transforms and allows 83 | // us to define keyForAttribute and keyForRelationship to handle 84 | // camelization correctly. 85 | this.normalize(type, payload[j]); 86 | self.extractDjangoPayload(store, type, payload[j]); 87 | } 88 | return payload; 89 | }, 90 | 91 | /** 92 | * This method allows you to push a single object payload. 93 | * 94 | * It will first normalize the payload, so you can use this to push 95 | * in data streaming in from your server structured the same way 96 | * that fetches and saves are structured. 97 | * 98 | * @param {DS.Store} store 99 | * @param {String} type 100 | * @param {Object} payload 101 | */ 102 | pushSinglePayload: function(store, type, payload) { 103 | type = store.modelFor(type); 104 | payload = this.extract(store, type, payload, null, "find"); 105 | store.push(type, payload); 106 | }, 107 | 108 | /** 109 | * This method allows you to push an array of object payloads. 110 | * 111 | * It will first normalize the payload, so you can use this to push 112 | * in data streaming in from your server structured the same way 113 | * that fetches and saves are structured. 114 | * 115 | * @param {DS.Store} store 116 | * @param {String} type 117 | * @param {Object} payload 118 | */ 119 | pushArrayPayload: function(store, type, payload) { 120 | type = store.modelFor(type); 121 | payload = this.extract(store, type, payload, null, "findAll"); 122 | store.pushMany(type, payload); 123 | }, 124 | 125 | /** 126 | * Converts camelcased attributes to underscored when serializing. 127 | * 128 | * Stolen from DS.ActiveModelSerializer. 129 | * 130 | * @method keyForAttribute 131 | * @param {String} attribute 132 | * @returns String 133 | */ 134 | keyForAttribute: function(attr) { 135 | return Ember.String.decamelize(attr); 136 | }, 137 | 138 | /** 139 | * Underscores relationship names when serializing relationship keys. 140 | * 141 | * Stolen from DS.ActiveModelSerializer. 142 | * 143 | * @method keyForRelationship 144 | * @param {String} key 145 | * @param {String} kind 146 | * @returns String 147 | */ 148 | keyForRelationship: function(key, kind) { 149 | return Ember.String.decamelize(key); 150 | }, 151 | 152 | /** 153 | * Adds support for skipping serialization of 154 | * DS.attr('foo', { readOnly: true }) 155 | * 156 | * @method serializeAttribute 157 | */ 158 | serializeAttribute: function(record, json, key, attribute) { 159 | if (!attribute.options.readOnly) { 160 | return this._super(record, json, key, attribute); 161 | } 162 | }, 163 | 164 | /** 165 | * Underscore relationship names when serializing belongsToRelationships 166 | * 167 | * @method serializeBelongsTo 168 | */ 169 | serializeBelongsTo: function(record, json, relationship) { 170 | var key = relationship.key; 171 | var belongsTo = record.get(key); 172 | var json_key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo") : key; 173 | 174 | if (Ember.isNone(belongsTo)) { 175 | json[json_key] = belongsTo; 176 | } else { 177 | if (typeof(record.get(key)) === 'string') { 178 | json[json_key] = record.get(key); 179 | }else{ 180 | json[json_key] = record.get(key).get('id'); 181 | } 182 | } 183 | 184 | if (relationship.options.polymorphic) { 185 | this.serializePolymorphicType(record, json, relationship); 186 | } 187 | }, 188 | 189 | /** 190 | * Underscore relationship names when serializing hasManyRelationships 191 | * 192 | * @method serializeHasMany 193 | */ 194 | serializeHasMany: function(record, json, relationship) { 195 | if (relationship.options.polymorphic) { 196 | // TODO implement once it's implemented in DS.JSONSerializer 197 | return; 198 | } 199 | 200 | var key = relationship.key, 201 | json_key = this.keyForRelationship(key, "hasMany"), 202 | relationshipType = DS.RelationshipChange ? DS.RelationshipChange.determineRelationshipType(record.constructor, relationship) : record.constructor.determineRelationshipType(relationship); 203 | 204 | if (relationshipType === 'manyToNone' || relationshipType === 'manyToMany') { 205 | json[json_key] = record.get(key).mapBy('id'); 206 | } 207 | }, 208 | 209 | /** 210 | * Add the key for a polymorphic relationship by adding `_type` to the 211 | * attribute and value from the model's underscored name. 212 | * 213 | * @method serializePolymorphicType 214 | * @param {DS.Model} record 215 | * @param {Object} json 216 | * @param {Object} relationship 217 | */ 218 | serializePolymorphicType: function(record, json, relationship) { 219 | var key = relationship.key, 220 | belongsTo = Ember.get(record, key); 221 | key = this.keyForAttribute ? this.keyForAttribute(key) : key; 222 | if(belongsTo) { 223 | json[this.keyForType(key)] = Ember.String.underscore(belongsTo.constructor.typeKey); 224 | } else { 225 | json[this.keyForType(key)] = null; 226 | } 227 | }, 228 | 229 | /** 230 | * Normalize: 231 | * 232 | * ```js 233 | * { 234 | * minion: "1" 235 | * minion_type: "evil_minion", 236 | * author: { 237 | * embeddedType: "user", 238 | * id: 1 239 | * } 240 | * } 241 | * ``` 242 | * 243 | * To: 244 | * 245 | * ```js 246 | * { 247 | * minion: "1" 248 | * minionType: "evil_minion" 249 | * author: { 250 | * type: "user", 251 | * id: 1 252 | * } 253 | * } 254 | * ``` 255 | * @method normalizeRelationships 256 | * @private 257 | */ 258 | normalizeRelationships: function(type,hash) { 259 | this._super.apply(this, arguments); 260 | 261 | if (this.keyForRelationship) { 262 | type.eachRelationship(function(key, relationship) { 263 | if (relationship.options.polymorphic) { 264 | var typeKey = this.keyForType(relationship.key); 265 | if(hash[typeKey]) { 266 | var typeKeyCamelCase = typeKey.replace(/_type$/,'Type'); 267 | hash[typeKeyCamelCase] = hash[typeKey]; 268 | delete hash[typeKey]; 269 | } 270 | 271 | if(hash[relationship.key]) { 272 | var embeddedData = hash[relationship.key]; 273 | var embeddedTypeKey = this.keyForEmbeddedType(relationship.key); 274 | if(embeddedTypeKey !== 'type') { 275 | if(Ember.isArray(embeddedData) && embeddedData.length) { 276 | map.call(embeddedData, function(obj,i) { 277 | this.normalizeTypeKey(obj,embeddedTypeKey); 278 | }, this); 279 | } else if(embeddedData[embeddedTypeKey]) { 280 | this.normalizeTypeKey(embeddedData,embeddedTypeKey); 281 | } 282 | } 283 | } 284 | } 285 | }, this); 286 | } 287 | }, 288 | 289 | /** 290 | * Replace a custom type key with a key named `type`. 291 | * 292 | * @method normalizeTypeKey 293 | * @param {Object} obj 294 | * @param {String} key 295 | */ 296 | normalizeTypeKey: function(obj,key) { 297 | obj.type = obj[key]; 298 | } 299 | }); 300 | -------------------------------------------------------------------------------- /tests/app.js: -------------------------------------------------------------------------------- 1 | /* global App: true */ 2 | App = Ember.Application.create({ 3 | rootElement: '#ember' 4 | }); 5 | 6 | App.SillyTransform = DS.Transform.extend({ 7 | text: "SILLYTRANSFORM", 8 | deserialize: function(serialized) { 9 | return serialized + this.text; 10 | }, 11 | serialize: function(deserialized) { 12 | return deserialized.slice(0, deserialized.length - this.text.length); 13 | } 14 | }); 15 | 16 | App.ObjectTransform = DS.Transform.extend({ 17 | deserialize: function(serialized) { 18 | return Ember.isEmpty(serialized) ? {} : JSON.parse(serialized); 19 | }, 20 | serialize: function(deserialized) { 21 | return Ember.isNone(deserialized) ? '' : JSON.stringify(deserialized); 22 | } 23 | }); 24 | 25 | App.Preserialized = DS.Model.extend({ 26 | // This will contain JSON that will be deserialized by the App.ObjectTransform. 27 | // If it deserializes to an array with anything other than numbers it will be 28 | // incorrectly interpreted by extractDjangoPayload as an embedded record. 29 | config: DS.attr('object') 30 | }); 31 | 32 | App.Transformer = DS.Model.extend({ 33 | transformed: DS.attr('silly') 34 | }); 35 | 36 | App.CamelUrl = DS.Model.extend({ 37 | test: DS.attr('string') 38 | }); 39 | 40 | App.Cart = DS.Model.extend({ 41 | name: DS.attr('string'), 42 | complete: DS.attr('boolean') 43 | }); 44 | 45 | App.Camel = DS.Model.extend({ 46 | camelCaseAttribute: DS.attr('string'), 47 | camelCaseRelationship: DS.hasMany('tag', { async: true }) 48 | }); 49 | 50 | App.Location = DS.Model.extend({ 51 | name:DS.attr('string') 52 | }); 53 | 54 | App.Session = DS.Model.extend({ 55 | name: DS.attr('string'), 56 | room: DS.attr('string'), 57 | tags: DS.hasMany('tag', {async: true }), 58 | speakers: DS.hasMany('speaker', { async: true }), 59 | ratings: DS.hasMany('rating', { async: true }) 60 | }); 61 | 62 | App.Speaker = DS.Model.extend({ 63 | name: DS.attr('string'), 64 | location: DS.attr('string'), 65 | association: DS.belongsTo('association'), 66 | personas: DS.hasMany('persona', { async: true }), 67 | badges: DS.hasMany('badge', { async: true }), 68 | session: DS.belongsTo('session'), 69 | zidentity: DS.belongsTo('user'), 70 | other: DS.belongsTo('other'), 71 | errors: '' 72 | }); 73 | 74 | App.Other = DS.Model.extend({ 75 | hat: DS.attr('string'), 76 | tags: DS.hasMany('tag'), 77 | speakers: DS.hasMany('speaker'), 78 | ratings: DS.hasMany('rating'), 79 | location: DS.belongsTo('location') 80 | }); 81 | 82 | App.Speaker.reopen({ 83 | becameError: function(errors) { 84 | var model = this.constructor.typeKey; 85 | this.set('errors', "operation failed for model: " + model); 86 | } 87 | }); 88 | 89 | App.Tag = DS.Model.extend({ 90 | description: DS.attr('string') 91 | }); 92 | 93 | App.Rating = DS.Model.extend({ 94 | score: DS.attr('number'), 95 | feedback: DS.attr('string'), 96 | session: DS.belongsTo('session'), 97 | other: DS.belongsTo('other') 98 | }); 99 | 100 | App.Association = DS.Model.extend({ 101 | name: DS.attr('string'), 102 | speakers: DS.hasMany('speaker', { async: true}) 103 | }); 104 | 105 | App.Author = DS.Model.extend({ 106 | }); 107 | 108 | App.User = App.Author.extend({ 109 | name: DS.attr('string'), 110 | username: DS.attr('string'), 111 | aliases: DS.hasMany('speaker', { async: true }), 112 | image: DS.attr('string', { readOnly: true }), 113 | messages: DS.hasMany('message', { polymorphic: true }) 114 | }); 115 | 116 | App.Company = App.Author.extend({ 117 | name: DS.attr('string'), 118 | sponsors: DS.hasMany('sponsor', { async: true}), 119 | messages: DS.hasMany('message', { polymorphic: true }), 120 | persona: DS.belongsTo('persona') 121 | }); 122 | 123 | App.Persona = DS.Model.extend({ 124 | nickname: DS.attr('string'), 125 | speaker: DS.belongsTo('speaker'), 126 | company: DS.belongsTo('company') 127 | }); 128 | 129 | App.Badge = DS.Model.extend({ 130 | city: DS.attr('string'), 131 | speaker: DS.belongsTo('speaker') 132 | }); 133 | 134 | App.Sponsor = DS.Model.extend({ 135 | name: DS.attr('string'), 136 | company: DS.belongsTo('company') 137 | }); 138 | 139 | App.CarPart = DS.Model.extend({ cars: DS.hasMany('car', {async: true})}); 140 | App.Car = DS.Model.extend({ carParts: DS.hasMany('carPart', {async: true})}); 141 | 142 | App.CamelParent = DS.Model.extend({ 143 | name: DS.attr('string'), 144 | camelKids: DS.hasMany('camelKid', {async: true}) 145 | }); 146 | 147 | App.CamelKid = DS.Model.extend({ 148 | description: DS.attr('string'), 149 | camelParent: DS.belongsTo('camelParent') 150 | }); 151 | 152 | App.Message = DS.Model.extend({ 153 | content: DS.attr('string'), 154 | author: DS.belongsTo('author', {polymorphic: true}), 155 | receiver: DS.belongsTo('author', {polymorphic: true}) 156 | }); 157 | 158 | App.Post = App.Message.extend({}); 159 | 160 | App.Comment = App.Message.extend({}); 161 | 162 | 163 | App.Obituary = DS.Model.extend({ 164 | publishOn: DS.attr('date'), 165 | timeOfDeath: DS.attr('datetime'), 166 | 167 | publishOnUtc: function() { 168 | if (!Ember.isEmpty(this.get('publishOn'))) { 169 | return this.get('publishOn').toUTCString(); 170 | } else { 171 | return ''; 172 | } 173 | }.property('publishOn'), 174 | 175 | timeOfDeathUtc: function() { 176 | if (!Ember.isEmpty(this.get('timeOfDeath'))) { 177 | return this.get('timeOfDeath').toUTCString(); 178 | } else { 179 | return ''; 180 | } 181 | }.property('timeOfDeath') 182 | }); 183 | 184 | 185 | App.NewObituaryController = Ember.Controller.extend({ 186 | publishOn: '', 187 | timeOfDeath: '', 188 | 189 | actions: { 190 | 191 | createObituary: function() { 192 | 193 | var newObituary = this.store.createRecord('obituary', { 194 | publishOn: new Date(this.get('publishOn')), 195 | timeOfDeath: new Date(this.get('timeOfDeath')), 196 | }); 197 | 198 | return newObituary.save(); 199 | } 200 | } 201 | }); 202 | 203 | 204 | App.ObituariesRoute = Ember.Route.extend({ 205 | 206 | model: function() { 207 | return this.store.find('obituary'); 208 | } 209 | }); 210 | 211 | 212 | App.ObituariesController = Ember.ArrayController.extend({ 213 | }); 214 | 215 | 216 | App.OthersRoute = Ember.Route.extend({ 217 | model: function() { 218 | return this.store.find('other'); 219 | } 220 | }); 221 | 222 | App.CamelParentRoute = Ember.Route.extend({ 223 | model: function() { 224 | return this.store.find('camelParent', 1); 225 | } 226 | }); 227 | 228 | App.CamelParentController = Ember.ObjectController.extend({ 229 | actions: { 230 | addCamelKid: function() { 231 | var parent = this.store.all('camelParent').objectAt(0); 232 | var hash = {'description': 'firstkid', 'camelParent': parent}; 233 | this.store.createRecord('camelKid', hash).save(); 234 | } 235 | } 236 | }); 237 | 238 | App.CartRoute = Ember.Route.extend({ 239 | model: function(params) { 240 | return this.store.find('cart', params.cart_id); 241 | } 242 | }); 243 | 244 | App.CartController = Ember.ObjectController.extend({ 245 | actions: { 246 | add: function() { 247 | var name = this.get('name'); 248 | var complete = this.get('complete'); 249 | var hash = {name: name, complete: complete}; 250 | this.store.createRecord('cart', hash).save(); 251 | } 252 | } 253 | }); 254 | 255 | App.RatingsRoute = Ember.Route.extend({ 256 | model: function() { 257 | return this.store.find('rating'); 258 | } 259 | }); 260 | 261 | App.PreserializedRoute = Ember.Route.extend({ 262 | model: function() { 263 | return this.store.find('preserialized'); 264 | } 265 | }); 266 | 267 | App.TransformersRoute = Ember.Route.extend({ 268 | model: function() { 269 | return this.store.find('transformer'); 270 | } 271 | }); 272 | 273 | App.CamelUrlsRoute = Ember.Route.extend({ 274 | model: function() { 275 | return this.store.find('camelUrl'); 276 | } 277 | }); 278 | 279 | App.CamelsRoute = Ember.Route.extend({ 280 | model: function() { 281 | return this.store.find('camel'); 282 | } 283 | }); 284 | 285 | App.SessionsRoute = Ember.Route.extend({ 286 | model: function() { 287 | return this.store.find('session'); 288 | } 289 | }); 290 | 291 | App.SpeakersRoute = Ember.Route.extend({ 292 | model: function() { 293 | return this.store.find('speaker', {name: 'Joel Taddei'}); 294 | } 295 | }); 296 | 297 | App.AssociationsRoute = Ember.Route.extend({ 298 | model: function() { 299 | return this.store.find('association'); 300 | } 301 | }); 302 | 303 | App.SpeakerController = Ember.ObjectController.extend({ 304 | actions: { 305 | updateSpeaker: function(model) { 306 | model.save().then(function() {}, function() { /* errors goes here */ }); 307 | } 308 | } 309 | }); 310 | 311 | App.OtherController = Ember.ObjectController.extend({ 312 | actions: { 313 | addRating: function(other) { 314 | var score = this.get('score'); 315 | var feedback = this.get('feedback'); 316 | if (score === undefined || feedback === undefined || Ember.$.trim(score) === "" || Ember.$.trim(feedback) === "") { 317 | return; 318 | } 319 | var rating = { score: score, feedback: feedback, other: other}; 320 | this.store.createRecord('rating', rating).save(); 321 | this.set('score', ''); 322 | this.set('feedback', ''); 323 | } 324 | } 325 | }); 326 | 327 | App.SessionController = Ember.ObjectController.extend({ 328 | actions: { 329 | addSpeaker: function(session) { 330 | var self = this; 331 | var name = this.get('speaker'); 332 | var location = this.get('location'); 333 | this.store.find('user', 1).then(function(user) { 334 | //to simulate a record create with multiple parents 335 | var hash = {zidentity: user, name: name, location: location, session: session}; 336 | self.store.createRecord('speaker', hash).save(); 337 | }); 338 | }, 339 | addSpeakerWithUserSingleParent: function(session) { 340 | var self = this; 341 | var name = this.get('speaker'); 342 | var location = this.get('location'); 343 | this.store.find('user', 1).then(function(user) { 344 | //to simulate a record create with single user parent 345 | var hash = {zidentity: user, name: name, location: location}; 346 | self.store.createRecord('speaker', hash).save(); 347 | }); 348 | }, 349 | addSpeakerWithSingleParent: function(session) { 350 | var self = this; 351 | var name = this.get('speaker'); 352 | var location = this.get('location'); 353 | //to simulate a record create with just a single parent 354 | var hash = {name: name, location: location, session: session}; 355 | self.store.createRecord('speaker', hash).save(); 356 | }, 357 | addRating: function(session) { 358 | var score = this.get('score'); 359 | var feedback = this.get('feedback'); 360 | if (score === undefined || feedback === undefined || Ember.$.trim(score) === "" || Ember.$.trim(feedback) === "") { 361 | return; 362 | } 363 | var rating = { score: score, feedback: feedback, session: session}; 364 | this.store.createRecord('rating', rating).save(); 365 | this.set('score', ''); 366 | this.set('feedback', ''); 367 | }, 368 | deleteRating: function(rating) { 369 | rating.deleteRecord(); 370 | rating.save(); 371 | } 372 | } 373 | }); 374 | 375 | App.Router.map(function() { 376 | this.resource("sessions", { path : "/sessions" }); 377 | this.resource("others", { path : "/others" }); 378 | this.resource("other", { path : "/other/:other_id" }); 379 | this.resource("associations", { path : "/associations" }); 380 | this.resource("speakers", { path : "/speakers" }); 381 | this.resource("ratings", { path : "/ratings" }); 382 | this.resource("session", { path : "/session/:session_id" }); 383 | this.resource("speaker", { path : "/speaker/:speaker_id" }); 384 | this.resource("cart", { path : "/cart/:cart_id" }); 385 | this.resource("camels", { path : "/camels" }); 386 | this.resource("camelParent", { path : "/camelParent" }); 387 | this.resource("camelUrls", { path : "/camelUrls" }); 388 | this.resource("transformers", { path : "/transformers" }); 389 | this.resource("tag", { path : "/tag/:tag_id" }); 390 | this.resource("user", { path : "/user/:user_id" }); 391 | this.resource("preserialized", { path: "/preserialized" }); 392 | this.resource('obituaries'); 393 | this.route('new-obituary'); 394 | }); 395 | 396 | App.ApplicationAdapter = DS.DjangoRESTAdapter.extend({ 397 | namespace: 'api' 398 | }); 399 | 400 | //monkey patch the ajax method for testing 401 | var ajaxUrl, ajaxType, ajaxHash; 402 | DS.DjangoRESTAdapter.reopen({ 403 | ajax: function(url, type, hash) { 404 | ajaxUrl = url; 405 | ajaxType = type; 406 | ajaxHash = hash; 407 | hash = hash || {}; 408 | hash.cache = false; 409 | return this._super(url, type, hash); 410 | } 411 | }); 412 | -------------------------------------------------------------------------------- /tests/adapter_tests.js: -------------------------------------------------------------------------------- 1 | var speakers_json, ratings_json, tags_json; 2 | 3 | module('integration tests', { 4 | setup: function() { 5 | ajaxUrl = undefined; 6 | ajaxType = undefined; 7 | ajaxHash = undefined; 8 | speakers_json = [{"id": 9, "name": "first", "session": 1}, {"id": 4, "name": "last", "session": 1}]; 9 | ratings_json = [{"id": 8, "score": 10, "feedback": "nice", "session": 1}]; 10 | tags_json = [{"id": 7, "description": "done"}]; 11 | App.reset(); 12 | }, 13 | teardown: function() { 14 | $.mockjaxClear(); 15 | } 16 | }); 17 | 18 | test('arrays as result of transform should not be interpreted as embedded records', function() { 19 | var json = [{"id": 1, "config": "[\"ember\",\"is\",\"neato\"]"}]; 20 | stubEndpointForHttpRequest('/api/preserializeds/', json); 21 | visit("/preserialized").then(function() { 22 | var divs = find("div.item").length; 23 | equal(divs, 3, "found " + divs + " divs"); 24 | var items = Ember.$.trim($("div.item").text()); 25 | equal(items, "emberisneato", "attribute was instead: " + items); 26 | }); 27 | }); 28 | 29 | test('attribute transforms are applied', function() { 30 | var json = [{"id": 1, "transformed": "blah blah"}]; 31 | stubEndpointForHttpRequest('/api/transformers/', json); 32 | visit("/transformers").then(function() { 33 | var spans = find("span").length; 34 | equal(spans, 1, "found " + spans + " spans"); 35 | var attribute = Ember.$.trim($("span.attribute").text()); 36 | equal(attribute, "blah blahSILLYTRANSFORM", "attribute was instead: " + attribute); 37 | }); 38 | }); 39 | 40 | test('models with camelCase converted to underscore urls', function() { 41 | var json = [{"id": 1, "test": "foobar"}]; 42 | stubEndpointForHttpRequest('/api/camel_urls/', json); 43 | visit("/camelUrls").then(function() { 44 | var spans = find("span").length; 45 | equal(spans, 1, "found " + spans + " spans"); 46 | var attribute = Ember.$.trim($("span.attribute").text()); 47 | equal(attribute, "foobar", "attribute was instead: " + attribute); 48 | }); 49 | }); 50 | 51 | test('keys with underscores converted to camelCase', function() { 52 | stubEndpointForHttpRequest('/api/camels/1/camel_case_relationship/', tags_json); 53 | var json = [{"id": 1, "camel_case_attribute": "foo", "camel_case_relationship": [7]}]; 54 | stubEndpointForHttpRequest('/api/camels/', json); 55 | visit("/camels").then(function() { 56 | var spans = find("span").length; 57 | equal(spans, 2, "found " + spans + " spans"); 58 | var attribute = Ember.$.trim($("span.attribute").text()); 59 | equal(attribute, "foo", "attribute was instead: " + attribute); 60 | var tag = Ember.$.trim($("span.tag").text()); 61 | equal(tag, "done", "tag was instead: " + tag); 62 | }); 63 | }); 64 | 65 | test('ajax response with 1 session yields table with 1 row', function() { 66 | var json = [{"id": 1, "name": "foo", "room": "bar", "desc": "test", "speakers": [], "ratings": [], "tags": []}]; 67 | stubEndpointForHttpRequest('/api/sessions/', json); 68 | visit("/sessions").then(function() { 69 | var rows = find("table tr").length; 70 | equal(rows, 6, "table had " + rows + " rows"); 71 | var name = Ember.$.trim($("table td.name").text()); 72 | equal(name, "foo", "name was instead: " + name); 73 | }); 74 | }); 75 | 76 | test('ajax response with no session records yields empty table', function() { 77 | stubEndpointForHttpRequest('/api/sessions/', []); 78 | visit("/sessions").then(function() { 79 | var rows = find("table tr").length; 80 | equal(rows, 0, "table had " + rows + " rows"); 81 | }); 82 | }); 83 | 84 | test('ajax response with async hasMany relationship renders correctly', function() { 85 | stubEndpointForHttpRequest('/api/sessions/1/speakers/', speakers_json); 86 | stubEndpointForHttpRequest('/api/sessions/1/ratings/', ratings_json); 87 | stubEndpointForHttpRequest('/api/sessions/1/tags/', tags_json); 88 | var json = [{"id": 1, "name": "foo", "room": "bar", "desc": "test", "speakers": [9,4], "ratings": [8], "tags": [7]}]; 89 | stubEndpointForHttpRequest('/api/sessions/', json); 90 | visit("/sessions").then(function() { 91 | //speakers 92 | var speakers = find("table td.speaker").length; 93 | equal(speakers, 2, "table had " + speakers + " speakers"); 94 | var speaker_one = Ember.$.trim($("table td.speaker:eq(0)").text()); 95 | equal(speaker_one, "first", "speaker_one was instead: " + speaker_one); 96 | var speaker_two = Ember.$.trim($("table td.speaker:eq(1)").text()); 97 | equal(speaker_two, "last", "speaker_two was instead: " + speaker_two); 98 | //ratings 99 | var ratings = find("table td.rating").length; 100 | equal(ratings, 1, "table had " + ratings + " ratings"); 101 | var rating_one = Ember.$.trim($("table td.rating:eq(0)").text()); 102 | equal(rating_one, "10", "rating_one was instead: " + rating_one); 103 | //tags 104 | var tags = find("table td.tag").length; 105 | equal(tags, 1, "table had " + tags + " tags"); 106 | var tag_one = Ember.$.trim($("table td.tag:eq(0)").text()); 107 | equal(tag_one, "done", "tag_one was instead: " + tag_one); 108 | }); 109 | }); 110 | 111 | test('ajax response for single session will render correctly', function() { 112 | stubEndpointForHttpRequest('/api/sessions/1/speakers/', speakers_json); 113 | stubEndpointForHttpRequest('/api/sessions/1/ratings/', ratings_json); 114 | stubEndpointForHttpRequest('/api/sessions/1/tags/', tags_json); 115 | var json = {"id": 1, "name": "foo", "room": "bar", "desc": "test", "speakers": [9,4], "ratings": [8], "tags": [7]}; 116 | stubEndpointForHttpRequest('/api/sessions/', [json]); 117 | stubEndpointForHttpRequest('/api/sessions/1/', json); 118 | visit("/session/1").then(function() { 119 | var name = Ember.$.trim($("div .model_name").text()); 120 | equal(name, "foo", "name was instead: " + name); 121 | //speakers 122 | var speakers = find("div .speakers span.name").length; 123 | equal(speakers, 2, "template had " + speakers + " speakers"); 124 | var speaker_one = Ember.$.trim($("div .speakers span.name:eq(0)").text()); 125 | equal(speaker_one, "first", "speaker_one was instead: " + speaker_one); 126 | var speaker_two = Ember.$.trim($("div .speakers span.name:eq(1)").text()); 127 | equal(speaker_two, "last", "speaker_two was instead: " + speaker_two); 128 | //ratings 129 | var ratings = find("div .ratings span.score").length; 130 | equal(ratings, 1, "table had " + ratings + " ratings"); 131 | var rating_one = Ember.$.trim($("div .ratings span.score:eq(0)").text()); 132 | equal(rating_one, "10", "rating_one was instead: " + rating_one); 133 | //setup the http post mock $.ajax 134 | //for some reason the 2 lines below are not used or needed? 135 | var response = {"id": 4, "score": 2, "feedback": "abc", "session": 1}; 136 | stubEndpointForHttpRequest('/api/ratings/', response, 'POST', 201); 137 | fillIn(".score", "2"); 138 | fillIn(".feedback", "abc"); 139 | return click(".add_rating"); 140 | }).then(function() { 141 | //this is currently broken for non-embedded bound templates (should be 2) 142 | var ratings = find("div .ratings span.score").length; 143 | equal(ratings, 1, "table had " + ratings + " ratings"); 144 | expectUrlTypeHashEqual("/api/ratings/", "POST", {}); 145 | expectRatingAddedToStore(4, 2, 'abc', 1); 146 | equal(ajaxHash.data, '{"score":2,"feedback":"abc","session":"1","other":null}'); 147 | }); 148 | }); 149 | 150 | test('test pushSinglePayload', function() { 151 | var json = {"id": 10, "description": "django"}; 152 | Ember.run(App, function(){ 153 | // load the object into the Ember data store 154 | var store = App.__container__.lookup("store:main"); // pretty sure this is not the right way to do this... 155 | store.serializerFor('tag').pushSinglePayload(store, 'tag', json); 156 | }); 157 | visit("/tag/10").then(function() { 158 | var content = Ember.$.trim($("span").text()); 159 | equal(content, "django", "name was instead: " + content); 160 | }); 161 | }); 162 | 163 | test('test pushArrayPayload', function() { 164 | var json = [{"id": 11, "description": "ember"}, {"id": 12, "description": "tomster"}]; 165 | Ember.run(App, function(){ 166 | // load the objects into the Ember data store 167 | var store = App.__container__.lookup("store:main"); // pretty sure this is not the right way to do this... 168 | store.serializerFor('tag').pushArrayPayload(store, 'tag', json); 169 | }); 170 | visit("/tag/12").then(function() { 171 | var content = Ember.$.trim($("span").text()); 172 | equal(content, "tomster", "name was instead: " + content); 173 | return visit("/tag/11"); 174 | }).then(function(){ 175 | var content = Ember.$.trim($("span").text()); 176 | equal(content, "ember", "name was instead: " + content); 177 | }); 178 | }); 179 | 180 | test('skip serializing readOnly attributes', function() { 181 | expect(1); 182 | 183 | var user = { 184 | 'id': 1, 185 | 'username': 'foo', 186 | 'image': 'http://example.org/foo.png' 187 | }; 188 | 189 | Ember.run(App, function() { 190 | var store = App.__container__.lookup('store:main'); 191 | var serializer = store.serializerFor('user'); 192 | serializer.pushSinglePayload(store, 'user', user); 193 | 194 | store.find('user', 1).then(function(record) { 195 | var result = serializer.serialize(record); 196 | equal(result.image, undefined); 197 | }); 198 | }); 199 | }); 200 | 201 | test('partial updates are sent as PATCH', function() { 202 | expect(1); 203 | 204 | var user = {"id": 1, "username": "foo", "image": "http://example.org/foo.png"}; 205 | 206 | Ember.run(App, function(){ 207 | var store = App.__container__.lookup("store:main"); 208 | var serializer = store.serializerFor('user'); 209 | serializer.pushSinglePayload(store, 'user', user); 210 | 211 | store.find('user', 1).then(function(record){ 212 | stubEndpointForHttpRequest('/api/users/1/', { 213 | "id": 1, 214 | "username": "patched", 215 | "image": "http://example.org/foo.png" 216 | }, "PATCH"); 217 | record.save().then(function(user){ 218 | equal(user.get("username"), "patched"); 219 | }); 220 | }); 221 | }); 222 | wait(); 223 | }); 224 | 225 | 226 | test('finding nested attributes when some requested records are already loaded makes GET request to the correct attribute-based URL', function() { 227 | var user = {"id": 1, "username": "foo", "aliases": [8, 9]}; 228 | var aliases = [{"id": 8, "name": "ember"}, {"id": 9, "name": "tomster"}]; 229 | Ember.run(App, function(){ 230 | // load the object into the Ember data store 231 | var store = App.__container__.lookup("store:main"); // pretty sure this is not the right way to do this... 232 | store.serializerFor('speaker').pushSinglePayload(store, 'speaker', aliases[0]); // pre-load the first alias object before find 233 | }); 234 | stubEndpointForHttpRequest('/api/users/1/', user); 235 | stubEndpointForHttpRequest('/api/users/1/aliases/', aliases); 236 | visit("/user/1").then(function() { 237 | var name = Ember.$.trim($(".username").text()); 238 | equal(name, "foo", "name was instead: " + name); 239 | var count = $(".alias").length; 240 | equal(count, 2, "count was instead: " + count); 241 | var alias = Ember.$.trim($(".alias:eq(0)").text()); 242 | equal(alias, "ember", "alias was instead: " + alias); 243 | }); 244 | }); 245 | 246 | test('finding nested attributes makes GET request to the correct attribute-based URL', function() { 247 | var user = {"id": 1, "username": "foo", "aliases": [8, 9]}; 248 | var aliases = [{"id": 8, "name": "ember"}, {"id": 9, "name": "tomster"}]; 249 | stubEndpointForHttpRequest('/api/users/1/', user); 250 | stubEndpointForHttpRequest('/api/users/1/aliases/', aliases); 251 | visit("/user/1").then(function() { 252 | var name = Ember.$.trim($(".username").text()); 253 | equal(name, "foo", "name was instead: " + name); 254 | var count = $(".alias").length; 255 | equal(count, 2, "count was instead: " + count); 256 | var alias = Ember.$.trim($(".alias:eq(0)").text()); 257 | equal(alias, "ember", "alias was instead: " + alias); 258 | }); 259 | }); 260 | 261 | test('basic error handling will bubble to the model', function() { 262 | var session = {"id": 1, "name": "x", "room": "y", "tags": [], ratings: [], speakers: [1]}; 263 | var speaker = {"id": 1, "name": "", "location": "iowa", "session": 1, "association": null, "personas": [1], "zidentity": null}; 264 | var personas = [{"id": 1, "nickname": "magic", "speaker": 1, "company": null}]; 265 | stubEndpointForHttpRequest('/api/sessions/1/', session); 266 | stubEndpointForHttpRequest('/api/speakers/1/', speaker); 267 | stubEndpointForHttpRequest('/api/speakers/1/personas/', personas); 268 | visit("/speaker/1").then(function() { 269 | var name = $("input.name").val(); 270 | equal(name, "", "name was instead: " + name); 271 | var errors = Ember.$.trim($("#errors").text()); 272 | equal(errors, "", "errors was instead: " + errors); 273 | stubEndpointForHttpRequest('/api/speakers/1/', {"name": ["This field is required."]}, 'PUT', 400); 274 | return click(".update"); 275 | }).then(function() { 276 | var name = $("input.name").val(); 277 | equal(name, "", "name was instead: " + name); 278 | var errors = Ember.$.trim($("#name-errors").text()); 279 | equal(errors, "This field is required.", "errors was instead: " + errors); 280 | }); 281 | }); 282 | 283 | test('basic error handling will not fire when update is successful', function() { 284 | stubEndpointForHttpRequest('/api/associations/1/', [{"id": 1, "name": "first", "speakers": [1]}]); 285 | stubEndpointForHttpRequest('/api/sessions/1/', [{"id": 1, "name": "z", "room": "d", "tags": [], "speakers": [1], "ratings": []}]); 286 | stubEndpointForHttpRequest('/api/users/1/', [{"id": 1, "username": "toranb", "aliases": []}]); 287 | var speaker = {"id": 1, "name": "wat", "location": "iowa", "session": 1, "association": 1, "personas": [1], "zidentity": 1}; 288 | var personas = [{"id": 1, "nickname": "magic", "speaker": 1, "company": 1}]; 289 | stubEndpointForHttpRequest('/api/speakers/1/', speaker); 290 | stubEndpointForHttpRequest('/api/speakers/1/personas/', personas); 291 | visit("/speaker/1").then(function() { 292 | var name = $("input.name").val(); 293 | equal(name, "wat", "name was instead: " + name); 294 | var errors = Ember.$.trim($("#errors").text()); 295 | equal(errors, "", "errors was instead: " + errors); 296 | }); 297 | // stubEndpointForHttpRequest('/api/speakers/1/', speaker, 'PUT', 200); 298 | // return click(".update"); 299 | // }).then(function() { 300 | // var name = $("input.name").val(); 301 | // equal(name, "wat", "name was instead: " + name); 302 | // var errors = Ember.$.trim($("#errors").text()); 303 | // equal(errors, "", "errors was instead: " + errors); 304 | // expectUrlTypeHashEqual("/api/speakers/1/", "PUT", speaker); 305 | // }); 306 | }); 307 | 308 | test('ajax post with multiple parents will use singular endpoint', function() { 309 | stubEndpointForHttpRequest('/api/users/1/aliases/', speakers_json); 310 | stubEndpointForHttpRequest('/api/sessions/1/speakers/', speakers_json); 311 | stubEndpointForHttpRequest('/api/sessions/1/ratings/', ratings_json); 312 | stubEndpointForHttpRequest('/api/sessions/1/tags/', tags_json); 313 | var json = {"id": 1, "name": "foo", "room": "bar", "desc": "test", "speakers": [9,4], "ratings": [8], "tags": [7]}; 314 | var response = {"id": 3, "name": "tom", "location": "iowa", "session": 1, "association": null, "personas": [], "zidentity": 1}; 315 | stubEndpointForHttpRequest('/api/sessions/', [json]); 316 | stubEndpointForHttpRequest('/api/sessions/1/', json); 317 | visit("/session/1").then(function() { 318 | var speakers = find("div .speakers span.name").length; 319 | equal(speakers, 2, "template had " + speakers + " speakers"); 320 | //setup the http post mock $.ajax 321 | var user = {"id": 1, "username": "toranb", "aliases": [1]}; 322 | stubEndpointForHttpRequest('/api/users/1/', user); 323 | stubEndpointForHttpRequest('/api/speakers/', response, 'POST', 201); 324 | fillIn(".speaker_name", "tom"); 325 | fillIn(".speaker_location", "iowa"); 326 | return click(".add_speaker"); 327 | }).then(function() { 328 | //this is currently broken for non-embedded bound templates (should be 3) 329 | var speakers = find("div .speakers span.name").length; 330 | equal(speakers, 2, "template had " + speakers + " speakers"); 331 | expectUrlTypeHashEqual("/api/speakers/", "POST", response); 332 | expectSpeakerAddedToStore(3, 'tom', 'iowa'); 333 | }); 334 | }); 335 | 336 | test('ajax post with single parent will use correctly nested endpoint', function() { 337 | stubEndpointForHttpRequest('/api/sessions/1/speakers/', speakers_json); 338 | stubEndpointForHttpRequest('/api/sessions/1/ratings/', ratings_json); 339 | stubEndpointForHttpRequest('/api/sessions/1/tags/', tags_json); 340 | var json = {"id": 1, "name": "foo", "room": "bar", "desc": "test", "speakers": [9,4], "ratings": [8], "tags": [7]}; 341 | var response = {"id": 3, "name": "axe", "location": "yo", "session": 1, "association": null, "personas": [], "zidentity": null}; 342 | stubEndpointForHttpRequest('/api/sessions/', [json]); 343 | stubEndpointForHttpRequest('/api/sessions/1/', json); 344 | visit("/session/1").then(function() { 345 | var speakers = find("div .speakers span.name").length; 346 | equal(speakers, 2, "template had " + speakers + " speakers"); 347 | //setup the http post mock $.ajax 348 | stubEndpointForHttpRequest('/api/speakers/', response, 'POST', 201); 349 | fillIn(".speaker_name", "tbill"); 350 | fillIn(".speaker_location", "ohio"); 351 | return click(".add_speaker_with_single_parent"); 352 | }).then(function() { 353 | //this is currently broken for non-embedded bound templates (should be 3) 354 | var speakers = find("div .speakers span.name").length; 355 | equal(speakers, 2, "template had " + speakers + " speakers"); 356 | expectUrlTypeHashEqual("/api/speakers/", "POST", response); 357 | expectSpeakerAddedToStore(3, 'axe', 'yo'); 358 | }); 359 | }); 360 | 361 | test('boolean values are sent over the wire correctly when value pulled from checkbox', function() { 362 | var cart = {id: 1, name: 'yup', complete: true}; 363 | stubEndpointForHttpRequest('/api/carts/1/', cart); 364 | visit("/cart/1").then(function() { 365 | var name = find(".name").val(); 366 | equal(name, "yup"); 367 | var complete = find(".complete").val(); 368 | equal(complete, 'on'); 369 | var response = {id: 1, name: 'newly', complete: true}; 370 | stubEndpointForHttpRequest('/api/carts/', response, 'POST', 201); 371 | fillIn(".name", "newly"); 372 | return click(".add"); 373 | }).then(function() { 374 | var name = find(".name").val(); 375 | equal(name, "newly"); 376 | var complete = find(".complete").val(); 377 | equal(complete, 'on'); 378 | equal(ajaxHash.url, '/api/carts/'); 379 | equal(ajaxHash.data, '{"name":"newly","complete":true}'); 380 | }); 381 | }); 382 | 383 | test('ajax post with different single parent will use correctly nested endpoint', function() { 384 | stubEndpointForHttpRequest('/api/users/1/aliases/', speakers_json); 385 | stubEndpointForHttpRequest('/api/sessions/1/speakers/', speakers_json); 386 | stubEndpointForHttpRequest('/api/sessions/1/ratings/', ratings_json); 387 | stubEndpointForHttpRequest('/api/sessions/1/tags/', tags_json); 388 | var json = {"id": 1, "name": "foo", "room": "bar", "desc": "test", "speakers": [9,4], "ratings": [8], "tags": [7]}; 389 | var response = {"id": 3, "name": "who", "location": "dat", "session": null, "association": null, "personas": [], "zidentity": 1}; 390 | stubEndpointForHttpRequest('/api/sessions/', [json]); 391 | stubEndpointForHttpRequest('/api/sessions/1/', json); 392 | visit("/session/1").then(function() { 393 | var speakers = find("div .speakers span.name").length; 394 | equal(speakers, 2, "template had " + speakers + " speakers"); 395 | //setup the http post mock $.ajax 396 | var user = {"id": 1, "username": "toranb", "aliases": [1]}; 397 | stubEndpointForHttpRequest('/api/users/1/', user); 398 | stubEndpointForHttpRequest('/api/speakers/', response, 'POST', 201); 399 | fillIn(".speaker_name", "who"); 400 | fillIn(".speaker_location", "dat"); 401 | return click(".add_speaker_with_user_single_parent"); 402 | }).then(function() { 403 | //this is currently broken for non-embedded bound templates (should be 3) 404 | var speakers = find("div .speakers span.name").length; 405 | equal(speakers, 2, "template had " + speakers + " speakers"); 406 | expectUrlTypeHashEqual("/api/speakers/", "POST", response); 407 | expectSpeakerAddedToStore(3, 'who', 'dat'); 408 | }); 409 | }); 410 | 411 | test('multiword hasMany key is serialized correctly on save', function() { 412 | var store = App.__container__.lookup('store:main'), car; 413 | stubEndpointForHttpRequest( 414 | '/api/cars/1/', 415 | {'id': 1, 'car_parts': [1,2]}, 'PUT'); 416 | 417 | Ember.run(function(){ 418 | var serializer = store.serializerFor('car'); 419 | serializer.pushSinglePayload(store, 'car', { 420 | 'id': 1, 'car_parts': [] 421 | }); 422 | serializer.pushArrayPayload(store, 'carPart', [ 423 | {'id': 1, 'cars': []}, 424 | {'id': 2, 'cars': []} 425 | ]); 426 | store.find('car', 1).then(function(result) { 427 | car = result; 428 | return Ember.RSVP.all([ 429 | store.find('carPart', 1), 430 | store.find('carPart', 2) 431 | ]); 432 | }).then(function(carParts) { 433 | car.set('carParts', carParts); 434 | return car.save(); 435 | }).then(function(car) { 436 | equal(ajaxHash.data, '{"car_parts":[]}'); 437 | return; 438 | }); 439 | }); 440 | 441 | wait(); 442 | }); 443 | 444 | test('camelCase belongsTo key is serialized with underscores on save', function() { 445 | var store = App.__container__.lookup('store:main'); 446 | stubEndpointForHttpRequest('/api/camel_parents/1/', {'id': 1, 'name': 'parent'}); 447 | visit("/camelParent").then(function() { 448 | stubEndpointForHttpRequest( 449 | '/api/camel_kids/', {"description":"firstkid","camel_parent":"1"}, 'POST', 201); 450 | return click(".add"); 451 | }).then(function() { 452 | equal(ajaxHash.data, '{"description":"firstkid","camel_parent":"1"}'); 453 | }); 454 | }); 455 | 456 | test('string ids are allowed', function() { 457 | var speaker = {"id": 1, "name": "wat", "location": "iowa", "session": 1, "badges": ["bna"], "association": 1, "personas": [], "zidentity": 1}; 458 | var badges = [{"id": "bna", "city": "Nashville"}]; 459 | stubEndpointForHttpRequest('/api/speakers/1/', speaker); 460 | stubEndpointForHttpRequest('/api/speakers/1/badges/', badges); 461 | visit("/speaker/1").then(function() { 462 | var city = $(".Nashville"); 463 | equal(city.length, 1, "One city was found"); 464 | equal(city.text(), "Nashville", "name was found: " + city.text()); 465 | }); 466 | }); 467 | -------------------------------------------------------------------------------- /tests/lib/jquery.mockjax.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * MockJax - jQuery Plugin to Mock Ajax requests 3 | * 4 | * Version: 1.5.3 5 | * Released: 6 | * Home: http://github.com/appendto/jquery-mockjax 7 | * Author: Jonathan Sharp (http://jdsharp.com) 8 | * License: MIT,GPL 9 | * 10 | * Copyright (c) 2011 appendTo LLC. 11 | * Dual licensed under the MIT or GPL licenses. 12 | * http://appendto.com/open-source-licenses 13 | */ 14 | (function($) { 15 | var _ajax = $.ajax, 16 | mockHandlers = [], 17 | mockedAjaxCalls = [], 18 | CALLBACK_REGEX = /=\?(&|$)/, 19 | jsc = (new Date()).getTime(); 20 | 21 | 22 | // Parse the given XML string. 23 | function parseXML(xml) { 24 | if ( window.DOMParser == undefined && window.ActiveXObject ) { 25 | DOMParser = function() { }; 26 | DOMParser.prototype.parseFromString = function( xmlString ) { 27 | var doc = new ActiveXObject('Microsoft.XMLDOM'); 28 | doc.async = 'false'; 29 | doc.loadXML( xmlString ); 30 | return doc; 31 | }; 32 | } 33 | 34 | try { 35 | var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' ); 36 | if ( $.isXMLDoc( xmlDoc ) ) { 37 | var err = $('parsererror', xmlDoc); 38 | if ( err.length == 1 ) { 39 | throw('Error: ' + $(xmlDoc).text() ); 40 | } 41 | } else { 42 | throw('Unable to parse XML'); 43 | } 44 | return xmlDoc; 45 | } catch( e ) { 46 | var msg = ( e.name == undefined ? e : e.name + ': ' + e.message ); 47 | $(document).trigger('xmlParseError', [ msg ]); 48 | return undefined; 49 | } 50 | } 51 | 52 | // Trigger a jQuery event 53 | function trigger(s, type, args) { 54 | (s.context ? $(s.context) : $.event).trigger(type, args); 55 | } 56 | 57 | // Check if the data field on the mock handler and the request match. This 58 | // can be used to restrict a mock handler to being used only when a certain 59 | // set of data is passed to it. 60 | function isMockDataEqual( mock, live ) { 61 | var identical = true; 62 | // Test for situations where the data is a querystring (not an object) 63 | if (typeof live === 'string') { 64 | // Querystring may be a regex 65 | return $.isFunction( mock.test ) ? mock.test(live) : mock == live; 66 | } 67 | $.each(mock, function(k) { 68 | if ( live[k] === undefined ) { 69 | identical = false; 70 | return identical; 71 | } else { 72 | // This will allow to compare Arrays 73 | if ( typeof live[k] === 'object' && live[k] !== null ) { 74 | identical = identical && isMockDataEqual(mock[k], live[k]); 75 | } else { 76 | if ( mock[k] && $.isFunction( mock[k].test ) ) { 77 | identical = identical && mock[k].test(live[k]); 78 | } else { 79 | identical = identical && ( mock[k] == live[k] ); 80 | } 81 | } 82 | } 83 | }); 84 | 85 | return identical; 86 | } 87 | 88 | // See if a mock handler property matches the default settings 89 | function isDefaultSetting(handler, property) { 90 | return handler[property] === $.mockjaxSettings[property]; 91 | } 92 | 93 | // Check the given handler should mock the given request 94 | function getMockForRequest( handler, requestSettings ) { 95 | // If the mock was registered with a function, let the function decide if we 96 | // want to mock this request 97 | if ( $.isFunction(handler) ) { 98 | return handler( requestSettings ); 99 | } 100 | 101 | // Inspect the URL of the request and check if the mock handler's url 102 | // matches the url for this ajax request 103 | if ( $.isFunction(handler.url.test) ) { 104 | // The user provided a regex for the url, test it 105 | if ( !handler.url.test( requestSettings.url ) ) { 106 | return null; 107 | } 108 | } else { 109 | // Look for a simple wildcard '*' or a direct URL match 110 | var star = handler.url.indexOf('*'); 111 | if (handler.url !== requestSettings.url && star === -1 || 112 | !new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace(/\*/g, '.+')).test(requestSettings.url)) { 113 | return null; 114 | } 115 | } 116 | 117 | // Inspect the data submitted in the request (either POST body or GET query string) 118 | if ( handler.data && requestSettings.data ) { 119 | if ( !isMockDataEqual(handler.data, requestSettings.data) ) { 120 | // They're not identical, do not mock this request 121 | return null; 122 | } 123 | } 124 | // Inspect the request type 125 | if ( handler && handler.type && 126 | handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) { 127 | // The request type doesn't match (GET vs. POST) 128 | return null; 129 | } 130 | 131 | return handler; 132 | } 133 | 134 | // Process the xhr objects send operation 135 | function _xhrSend(mockHandler, requestSettings, origSettings) { 136 | 137 | // This is a substitute for < 1.4 which lacks $.proxy 138 | var process = (function(that) { 139 | return function() { 140 | return (function() { 141 | var onReady; 142 | 143 | // The request has returned 144 | this.status = mockHandler.status; 145 | this.statusText = mockHandler.statusText; 146 | this.readyState = 4; 147 | 148 | // We have an executable function, call it to give 149 | // the mock handler a chance to update it's data 150 | if ( $.isFunction(mockHandler.response) ) { 151 | mockHandler.response(origSettings); 152 | } 153 | // Copy over our mock to our xhr object before passing control back to 154 | // jQuery's onreadystatechange callback 155 | if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) { 156 | this.responseText = JSON.stringify(mockHandler.responseText); 157 | } else if ( requestSettings.dataType == 'xml' ) { 158 | if ( typeof mockHandler.responseXML == 'string' ) { 159 | this.responseXML = parseXML(mockHandler.responseXML); 160 | //in jQuery 1.9.1+, responseXML is processed differently and relies on responseText 161 | this.responseText = mockHandler.responseXML; 162 | } else { 163 | this.responseXML = mockHandler.responseXML; 164 | } 165 | } else { 166 | this.responseText = mockHandler.responseText; 167 | } 168 | if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) { 169 | this.status = mockHandler.status; 170 | } 171 | if( typeof mockHandler.statusText === "string") { 172 | this.statusText = mockHandler.statusText; 173 | } 174 | // jQuery 2.0 renamed onreadystatechange to onload 175 | onReady = this.onreadystatechange || this.onload; 176 | 177 | // jQuery < 1.4 doesn't have onreadystate change for xhr 178 | if ( $.isFunction( onReady ) ) { 179 | if( mockHandler.isTimeout) { 180 | this.status = -1; 181 | } 182 | onReady.call( this, mockHandler.isTimeout ? 'timeout' : undefined ); 183 | } else if ( mockHandler.isTimeout ) { 184 | // Fix for 1.3.2 timeout to keep success from firing. 185 | this.status = -1; 186 | } 187 | }).apply(that); 188 | }; 189 | })(this); 190 | 191 | if ( mockHandler.proxy ) { 192 | // We're proxying this request and loading in an external file instead 193 | _ajax({ 194 | global: false, 195 | url: mockHandler.proxy, 196 | type: mockHandler.proxyType, 197 | data: mockHandler.data, 198 | dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType, 199 | complete: function(xhr) { 200 | mockHandler.responseXML = xhr.responseXML; 201 | mockHandler.responseText = xhr.responseText; 202 | // Don't override the handler status/statusText if it's specified by the config 203 | if (isDefaultSetting(mockHandler, 'status')) { 204 | mockHandler.status = xhr.status; 205 | } 206 | if (isDefaultSetting(mockHandler, 'statusText')) { 207 | mockHandler.statusText = xhr.statusText; 208 | } 209 | 210 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 0); 211 | } 212 | }); 213 | } else { 214 | // type == 'POST' || 'GET' || 'DELETE' 215 | if ( requestSettings.async === false ) { 216 | // TODO: Blocking delay 217 | process(); 218 | } else { 219 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 50); 220 | } 221 | } 222 | } 223 | 224 | // Construct a mocked XHR Object 225 | function xhr(mockHandler, requestSettings, origSettings, origHandler) { 226 | // Extend with our default mockjax settings 227 | mockHandler = $.extend(true, {}, $.mockjaxSettings, mockHandler); 228 | 229 | if (typeof mockHandler.headers === 'undefined') { 230 | mockHandler.headers = {}; 231 | } 232 | if ( mockHandler.contentType ) { 233 | mockHandler.headers['content-type'] = mockHandler.contentType; 234 | } 235 | 236 | return { 237 | status: mockHandler.status, 238 | statusText: mockHandler.statusText, 239 | readyState: 1, 240 | open: function() { }, 241 | send: function() { 242 | origHandler.fired = true; 243 | _xhrSend.call(this, mockHandler, requestSettings, origSettings); 244 | }, 245 | abort: function() { 246 | clearTimeout(this.responseTimer); 247 | }, 248 | setRequestHeader: function(header, value) { 249 | mockHandler.headers[header] = value; 250 | }, 251 | getResponseHeader: function(header) { 252 | // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery 253 | if ( mockHandler.headers && mockHandler.headers[header] ) { 254 | // Return arbitrary headers 255 | return mockHandler.headers[header]; 256 | } else if ( header.toLowerCase() == 'last-modified' ) { 257 | return mockHandler.lastModified || (new Date()).toString(); 258 | } else if ( header.toLowerCase() == 'etag' ) { 259 | return mockHandler.etag || ''; 260 | } else if ( header.toLowerCase() == 'content-type' ) { 261 | return mockHandler.contentType || 'text/plain'; 262 | } 263 | }, 264 | getAllResponseHeaders: function() { 265 | var headers = ''; 266 | $.each(mockHandler.headers, function(k, v) { 267 | headers += k + ': ' + v + "\n"; 268 | }); 269 | return headers; 270 | } 271 | }; 272 | } 273 | 274 | // Process a JSONP mock request. 275 | function processJsonpMock( requestSettings, mockHandler, origSettings ) { 276 | // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here 277 | // because there isn't an easy hook for the cross domain script tag of jsonp 278 | 279 | processJsonpUrl( requestSettings ); 280 | 281 | requestSettings.dataType = "json"; 282 | if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) { 283 | createJsonpCallback(requestSettings, mockHandler, origSettings); 284 | 285 | // We need to make sure 286 | // that a JSONP style response is executed properly 287 | 288 | var rurl = /^(\w+:)?\/\/([^\/?#]+)/, 289 | parts = rurl.exec( requestSettings.url ), 290 | remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); 291 | 292 | requestSettings.dataType = "script"; 293 | if(requestSettings.type.toUpperCase() === "GET" && remote ) { 294 | var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings ); 295 | 296 | // Check if we are supposed to return a Deferred back to the mock call, or just 297 | // signal success 298 | if(newMockReturn) { 299 | return newMockReturn; 300 | } else { 301 | return true; 302 | } 303 | } 304 | } 305 | return null; 306 | } 307 | 308 | // Append the required callback parameter to the end of the request URL, for a JSONP request 309 | function processJsonpUrl( requestSettings ) { 310 | if ( requestSettings.type.toUpperCase() === "GET" ) { 311 | if ( !CALLBACK_REGEX.test( requestSettings.url ) ) { 312 | requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") + 313 | (requestSettings.jsonp || "callback") + "=?"; 314 | } 315 | } else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) { 316 | requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?"; 317 | } 318 | } 319 | 320 | // Process a JSONP request by evaluating the mocked response text 321 | function processJsonpRequest( requestSettings, mockHandler, origSettings ) { 322 | // Synthesize the mock request for adding a script tag 323 | var callbackContext = origSettings && origSettings.context || requestSettings, 324 | newMock = null; 325 | 326 | 327 | // If the response handler on the moock is a function, call it 328 | if ( mockHandler.response && $.isFunction(mockHandler.response) ) { 329 | mockHandler.response(origSettings); 330 | } else { 331 | 332 | // Evaluate the responseText javascript in a global context 333 | if( typeof mockHandler.responseText === 'object' ) { 334 | $.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')'); 335 | } else { 336 | $.globalEval( '(' + mockHandler.responseText + ')'); 337 | } 338 | } 339 | 340 | // Successful response 341 | jsonpSuccess( requestSettings, callbackContext, mockHandler ); 342 | jsonpComplete( requestSettings, callbackContext, mockHandler ); 343 | 344 | // If we are running under jQuery 1.5+, return a deferred object 345 | if($.Deferred){ 346 | newMock = new $.Deferred(); 347 | if(typeof mockHandler.responseText == "object"){ 348 | newMock.resolveWith( callbackContext, [mockHandler.responseText] ); 349 | } 350 | else{ 351 | newMock.resolveWith( callbackContext, [$.parseJSON( mockHandler.responseText )] ); 352 | } 353 | } 354 | return newMock; 355 | } 356 | 357 | 358 | // Create the required JSONP callback function for the request 359 | function createJsonpCallback( requestSettings, mockHandler, origSettings ) { 360 | var callbackContext = origSettings && origSettings.context || requestSettings; 361 | var jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++); 362 | 363 | // Replace the =? sequence both in the query string and the data 364 | if ( requestSettings.data ) { 365 | requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 366 | } 367 | 368 | requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 369 | 370 | 371 | // Handle JSONP-style loading 372 | window[ jsonp ] = window[ jsonp ] || function( tmp ) { 373 | data = tmp; 374 | jsonpSuccess( requestSettings, callbackContext, mockHandler ); 375 | jsonpComplete( requestSettings, callbackContext, mockHandler ); 376 | // Garbage collect 377 | window[ jsonp ] = undefined; 378 | 379 | try { 380 | delete window[ jsonp ]; 381 | } catch(e) {} 382 | 383 | if ( head ) { 384 | head.removeChild( script ); 385 | } 386 | }; 387 | } 388 | 389 | // The JSONP request was successful 390 | function jsonpSuccess(requestSettings, callbackContext, mockHandler) { 391 | // If a local callback was specified, fire it and pass it the data 392 | if ( requestSettings.success ) { 393 | requestSettings.success.call( callbackContext, mockHandler.responseText || "", status, {} ); 394 | } 395 | 396 | // Fire the global callback 397 | if ( requestSettings.global ) { 398 | trigger(requestSettings, "ajaxSuccess", [{}, requestSettings] ); 399 | } 400 | } 401 | 402 | // The JSONP request was completed 403 | function jsonpComplete(requestSettings, callbackContext) { 404 | // Process result 405 | if ( requestSettings.complete ) { 406 | requestSettings.complete.call( callbackContext, {} , status ); 407 | } 408 | 409 | // The request was completed 410 | if ( requestSettings.global ) { 411 | trigger( "ajaxComplete", [{}, requestSettings] ); 412 | } 413 | 414 | // Handle the global AJAX counter 415 | if ( requestSettings.global && ! --$.active ) { 416 | $.event.trigger( "ajaxStop" ); 417 | } 418 | } 419 | 420 | 421 | // The core $.ajax replacement. 422 | function handleAjax( url, origSettings ) { 423 | var mockRequest, requestSettings, mockHandler; 424 | 425 | // If url is an object, simulate pre-1.5 signature 426 | if ( typeof url === "object" ) { 427 | origSettings = url; 428 | url = undefined; 429 | } else { 430 | // work around to support 1.5 signature 431 | origSettings.url = url; 432 | } 433 | 434 | // Extend the original settings for the request 435 | requestSettings = $.extend(true, {}, $.ajaxSettings, origSettings); 436 | 437 | // Iterate over our mock handlers (in registration order) until we find 438 | // one that is willing to intercept the request 439 | for(var k = 0; k < mockHandlers.length; k++) { 440 | if ( !mockHandlers[k] ) { 441 | continue; 442 | } 443 | 444 | mockHandler = getMockForRequest( mockHandlers[k], requestSettings ); 445 | if(!mockHandler) { 446 | // No valid mock found for this request 447 | continue; 448 | } 449 | 450 | mockedAjaxCalls.push(requestSettings); 451 | 452 | // If logging is enabled, log the mock to the console 453 | $.mockjaxSettings.log( mockHandler, requestSettings ); 454 | 455 | 456 | if ( requestSettings.dataType === "jsonp" ) { 457 | if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) { 458 | // This mock will handle the JSONP request 459 | return mockRequest; 460 | } 461 | } 462 | 463 | 464 | // Removed to fix #54 - keep the mocking data object intact 465 | //mockHandler.data = requestSettings.data; 466 | 467 | mockHandler.cache = requestSettings.cache; 468 | mockHandler.timeout = requestSettings.timeout; 469 | mockHandler.global = requestSettings.global; 470 | 471 | copyUrlParameters(mockHandler, origSettings); 472 | 473 | (function(mockHandler, requestSettings, origSettings, origHandler) { 474 | mockRequest = _ajax.call($, $.extend(true, {}, origSettings, { 475 | // Mock the XHR object 476 | xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ); } 477 | })); 478 | })(mockHandler, requestSettings, origSettings, mockHandlers[k]); 479 | 480 | return mockRequest; 481 | } 482 | 483 | // We don't have a mock request 484 | if($.mockjaxSettings.throwUnmocked === true) { 485 | throw('AJAX not mocked: ' + origSettings.url); 486 | } 487 | else { // trigger a normal request 488 | return _ajax.apply($, [origSettings]); 489 | } 490 | } 491 | 492 | /** 493 | * Copies URL parameter values if they were captured by a regular expression 494 | * @param {Object} mockHandler 495 | * @param {Object} origSettings 496 | */ 497 | function copyUrlParameters(mockHandler, origSettings) { 498 | //parameters aren't captured if the URL isn't a RegExp 499 | if (!(mockHandler.url instanceof RegExp)) { 500 | return; 501 | } 502 | //if no URL params were defined on the handler, don't attempt a capture 503 | if (!mockHandler.hasOwnProperty('urlParams')) { 504 | return; 505 | } 506 | var captures = mockHandler.url.exec(origSettings.url); 507 | //the whole RegExp match is always the first value in the capture results 508 | if (captures.length === 1) { 509 | return; 510 | } 511 | captures.shift(); 512 | //use handler params as keys and capture resuts as values 513 | var i = 0, 514 | capturesLength = captures.length, 515 | paramsLength = mockHandler.urlParams.length, 516 | //in case the number of params specified is less than actual captures 517 | maxIterations = Math.min(capturesLength, paramsLength), 518 | paramValues = {}; 519 | for (i; i < maxIterations; i++) { 520 | var key = mockHandler.urlParams[i]; 521 | paramValues[key] = captures[i]; 522 | } 523 | origSettings.urlParams = paramValues; 524 | } 525 | 526 | 527 | // Public 528 | 529 | $.extend({ 530 | ajax: handleAjax 531 | }); 532 | 533 | $.mockjaxSettings = { 534 | //url: null, 535 | //type: 'GET', 536 | log: function( mockHandler, requestSettings ) { 537 | if ( mockHandler.logging === false || 538 | ( typeof mockHandler.logging === 'undefined' && $.mockjaxSettings.logging === false ) ) { 539 | return; 540 | } 541 | if ( window.console && console.log ) { 542 | var message = 'MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url; 543 | var request = $.extend({}, requestSettings); 544 | 545 | if (typeof console.log === 'function') { 546 | console.log(message, request); 547 | } else { 548 | try { 549 | console.log( message + ' ' + JSON.stringify(request) ); 550 | } catch (e) { 551 | console.log(message); 552 | } 553 | } 554 | } 555 | }, 556 | logging: true, 557 | status: 200, 558 | statusText: "OK", 559 | responseTime: 500, 560 | isTimeout: false, 561 | throwUnmocked: false, 562 | contentType: 'text/plain', 563 | response: '', 564 | responseText: '', 565 | responseXML: '', 566 | proxy: '', 567 | proxyType: 'GET', 568 | 569 | lastModified: null, 570 | etag: '', 571 | headers: { 572 | etag: 'IJF@H#@923uf8023hFO@I#H#', 573 | 'content-type' : 'text/plain' 574 | } 575 | }; 576 | 577 | $.mockjax = function(settings) { 578 | var i = mockHandlers.length; 579 | mockHandlers[i] = settings; 580 | return i; 581 | }; 582 | $.mockjaxClear = function(i) { 583 | if ( arguments.length == 1 ) { 584 | mockHandlers[i] = null; 585 | } else { 586 | mockHandlers = []; 587 | } 588 | mockedAjaxCalls = []; 589 | }; 590 | $.mockjax.handler = function(i) { 591 | if ( arguments.length == 1 ) { 592 | return mockHandlers[i]; 593 | } 594 | }; 595 | $.mockjax.mockedAjaxCalls = function() { 596 | return mockedAjaxCalls; 597 | }; 598 | })(jQuery); 599 | -------------------------------------------------------------------------------- /tests/lib/handlebars-v1.2.1.js: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | handlebars v1.2.1 4 | 5 | Copyright (C) 2011 by Yehuda Katz 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | @license 26 | */ 27 | /* exported Handlebars */ 28 | var Handlebars = (function() { 29 | // handlebars/safe-string.js 30 | var __module4__ = (function() { 31 | "use strict"; 32 | var __exports__; 33 | // Build out our basic SafeString type 34 | function SafeString(string) { 35 | this.string = string; 36 | } 37 | 38 | SafeString.prototype.toString = function() { 39 | return "" + this.string; 40 | }; 41 | 42 | __exports__ = SafeString; 43 | return __exports__; 44 | })(); 45 | 46 | // handlebars/utils.js 47 | var __module3__ = (function(__dependency1__) { 48 | "use strict"; 49 | var __exports__ = {}; 50 | /*jshint -W004 */ 51 | var SafeString = __dependency1__; 52 | 53 | var escape = { 54 | "&": "&", 55 | "<": "<", 56 | ">": ">", 57 | '"': """, 58 | "'": "'", 59 | "`": "`" 60 | }; 61 | 62 | var badChars = /[&<>"'`]/g; 63 | var possible = /[&<>"'`]/; 64 | 65 | function escapeChar(chr) { 66 | return escape[chr] || "&"; 67 | } 68 | 69 | function extend(obj, value) { 70 | for(var key in value) { 71 | if(Object.prototype.hasOwnProperty.call(value, key)) { 72 | obj[key] = value[key]; 73 | } 74 | } 75 | } 76 | 77 | __exports__.extend = extend;var toString = Object.prototype.toString; 78 | __exports__.toString = toString; 79 | // Sourced from lodash 80 | // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt 81 | var isFunction = function(value) { 82 | return typeof value === 'function'; 83 | }; 84 | // fallback for older versions of Chrome and Safari 85 | if (isFunction(/x/)) { 86 | isFunction = function(value) { 87 | return typeof value === 'function' && toString.call(value) === '[object Function]'; 88 | }; 89 | } 90 | var isFunction; 91 | __exports__.isFunction = isFunction; 92 | var isArray = Array.isArray || function(value) { 93 | return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; 94 | }; 95 | __exports__.isArray = isArray; 96 | 97 | function escapeExpression(string) { 98 | // don't escape SafeStrings, since they're already safe 99 | if (string instanceof SafeString) { 100 | return string.toString(); 101 | } else if (!string && string !== 0) { 102 | return ""; 103 | } 104 | 105 | // Force a string conversion as this will be done by the append regardless and 106 | // the regex test will do this transparently behind the scenes, causing issues if 107 | // an object's to string has escaped characters in it. 108 | string = "" + string; 109 | 110 | if(!possible.test(string)) { return string; } 111 | return string.replace(badChars, escapeChar); 112 | } 113 | 114 | __exports__.escapeExpression = escapeExpression;function isEmpty(value) { 115 | if (!value && value !== 0) { 116 | return true; 117 | } else if (isArray(value) && value.length === 0) { 118 | return true; 119 | } else { 120 | return false; 121 | } 122 | } 123 | 124 | __exports__.isEmpty = isEmpty; 125 | return __exports__; 126 | })(__module4__); 127 | 128 | // handlebars/exception.js 129 | var __module5__ = (function() { 130 | "use strict"; 131 | var __exports__; 132 | 133 | var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; 134 | 135 | function Exception(/* message */) { 136 | var tmp = Error.prototype.constructor.apply(this, arguments); 137 | 138 | // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. 139 | for (var idx = 0; idx < errorProps.length; idx++) { 140 | this[errorProps[idx]] = tmp[errorProps[idx]]; 141 | } 142 | } 143 | 144 | Exception.prototype = new Error(); 145 | 146 | __exports__ = Exception; 147 | return __exports__; 148 | })(); 149 | 150 | // handlebars/base.js 151 | var __module2__ = (function(__dependency1__, __dependency2__) { 152 | "use strict"; 153 | var __exports__ = {}; 154 | var Utils = __dependency1__; 155 | var Exception = __dependency2__; 156 | 157 | var VERSION = "1.2.1"; 158 | __exports__.VERSION = VERSION;var COMPILER_REVISION = 4; 159 | __exports__.COMPILER_REVISION = COMPILER_REVISION; 160 | var REVISION_CHANGES = { 161 | 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 162 | 2: '== 1.0.0-rc.3', 163 | 3: '== 1.0.0-rc.4', 164 | 4: '>= 1.0.0' 165 | }; 166 | __exports__.REVISION_CHANGES = REVISION_CHANGES; 167 | var isArray = Utils.isArray, 168 | isFunction = Utils.isFunction, 169 | toString = Utils.toString, 170 | objectType = '[object Object]'; 171 | 172 | function HandlebarsEnvironment(helpers, partials) { 173 | this.helpers = helpers || {}; 174 | this.partials = partials || {}; 175 | 176 | registerDefaultHelpers(this); 177 | } 178 | 179 | __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { 180 | constructor: HandlebarsEnvironment, 181 | 182 | logger: logger, 183 | log: log, 184 | 185 | registerHelper: function(name, fn, inverse) { 186 | if (toString.call(name) === objectType) { 187 | if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); } 188 | Utils.extend(this.helpers, name); 189 | } else { 190 | if (inverse) { fn.not = inverse; } 191 | this.helpers[name] = fn; 192 | } 193 | }, 194 | 195 | registerPartial: function(name, str) { 196 | if (toString.call(name) === objectType) { 197 | Utils.extend(this.partials, name); 198 | } else { 199 | this.partials[name] = str; 200 | } 201 | } 202 | }; 203 | 204 | function registerDefaultHelpers(instance) { 205 | instance.registerHelper('helperMissing', function(arg) { 206 | if(arguments.length === 2) { 207 | return undefined; 208 | } else { 209 | throw new Error("Missing helper: '" + arg + "'"); 210 | } 211 | }); 212 | 213 | instance.registerHelper('blockHelperMissing', function(context, options) { 214 | var inverse = options.inverse || function() {}, fn = options.fn; 215 | 216 | if (isFunction(context)) { context = context.call(this); } 217 | 218 | if(context === true) { 219 | return fn(this); 220 | } else if(context === false || context == null) { 221 | return inverse(this); 222 | } else if (isArray(context)) { 223 | if(context.length > 0) { 224 | return instance.helpers.each(context, options); 225 | } else { 226 | return inverse(this); 227 | } 228 | } else { 229 | return fn(context); 230 | } 231 | }); 232 | 233 | instance.registerHelper('each', function(context, options) { 234 | var fn = options.fn, inverse = options.inverse; 235 | var i = 0, ret = "", data; 236 | 237 | if (isFunction(context)) { context = context.call(this); } 238 | 239 | if (options.data) { 240 | data = createFrame(options.data); 241 | } 242 | 243 | if(context && typeof context === 'object') { 244 | if (isArray(context)) { 245 | for(var j = context.length; i 0) { throw new Exception("Invalid path: " + original); } 621 | else if (part === "..") { depth++; } 622 | else { this.isScoped = true; } 623 | } 624 | else { dig.push(part); } 625 | } 626 | 627 | this.original = original; 628 | this.parts = dig; 629 | this.string = dig.join('.'); 630 | this.depth = depth; 631 | 632 | // an ID is simple if it only has one part, and that part is not 633 | // `..` or `this`. 634 | this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; 635 | 636 | this.stringModeValue = this.string; 637 | }, 638 | 639 | PartialNameNode: function(name) { 640 | this.type = "PARTIAL_NAME"; 641 | this.name = name.original; 642 | }, 643 | 644 | DataNode: function(id) { 645 | this.type = "DATA"; 646 | this.id = id; 647 | }, 648 | 649 | StringNode: function(string) { 650 | this.type = "STRING"; 651 | this.original = 652 | this.string = 653 | this.stringModeValue = string; 654 | }, 655 | 656 | IntegerNode: function(integer) { 657 | this.type = "INTEGER"; 658 | this.original = 659 | this.integer = integer; 660 | this.stringModeValue = Number(integer); 661 | }, 662 | 663 | BooleanNode: function(bool) { 664 | this.type = "BOOLEAN"; 665 | this.bool = bool; 666 | this.stringModeValue = bool === "true"; 667 | }, 668 | 669 | CommentNode: function(comment) { 670 | this.type = "comment"; 671 | this.comment = comment; 672 | } 673 | }; 674 | 675 | // Must be exported as an object rather than the root of the module as the jison lexer 676 | // most modify the object to operate properly. 677 | __exports__ = AST; 678 | return __exports__; 679 | })(__module5__); 680 | 681 | // handlebars/compiler/parser.js 682 | var __module9__ = (function() { 683 | "use strict"; 684 | var __exports__; 685 | /* jshint ignore:start */ 686 | /* Jison generated parser */ 687 | var handlebars = (function(){ 688 | var parser = {trace: function trace() { }, 689 | yy: {}, 690 | symbols_: {"error":2,"root":3,"statements":4,"EOF":5,"program":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"CLOSE_UNESCAPED":24,"OPEN_PARTIAL":25,"partialName":26,"partial_option0":27,"inMustache_repetition0":28,"inMustache_option0":29,"dataName":30,"param":31,"STRING":32,"INTEGER":33,"BOOLEAN":34,"hash":35,"hash_repetition_plus0":36,"hashSegment":37,"ID":38,"EQUALS":39,"DATA":40,"pathSegments":41,"SEP":42,"$accept":0,"$end":1}, 691 | terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"CLOSE_UNESCAPED",25:"OPEN_PARTIAL",32:"STRING",33:"INTEGER",34:"BOOLEAN",38:"ID",39:"EQUALS",40:"DATA",42:"SEP"}, 692 | productions_: [0,[3,2],[3,1],[6,2],[6,3],[6,2],[6,1],[6,1],[6,0],[4,1],[4,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,4],[7,2],[17,3],[17,1],[31,1],[31,1],[31,1],[31,1],[31,1],[35,1],[37,3],[26,1],[26,1],[26,1],[30,2],[21,1],[41,3],[41,1],[27,0],[27,1],[28,0],[28,2],[29,0],[29,1],[36,1],[36,2]], 693 | performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { 694 | 695 | var $0 = $$.length - 1; 696 | switch (yystate) { 697 | case 1: return new yy.ProgramNode($$[$0-1]); 698 | break; 699 | case 2: return new yy.ProgramNode([]); 700 | break; 701 | case 3:this.$ = new yy.ProgramNode([], $$[$0-1], $$[$0]); 702 | break; 703 | case 4:this.$ = new yy.ProgramNode($$[$0-2], $$[$0-1], $$[$0]); 704 | break; 705 | case 5:this.$ = new yy.ProgramNode($$[$0-1], $$[$0], []); 706 | break; 707 | case 6:this.$ = new yy.ProgramNode($$[$0]); 708 | break; 709 | case 7:this.$ = new yy.ProgramNode([]); 710 | break; 711 | case 8:this.$ = new yy.ProgramNode([]); 712 | break; 713 | case 9:this.$ = [$$[$0]]; 714 | break; 715 | case 10: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; 716 | break; 717 | case 11:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]); 718 | break; 719 | case 12:this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]); 720 | break; 721 | case 13:this.$ = $$[$0]; 722 | break; 723 | case 14:this.$ = $$[$0]; 724 | break; 725 | case 15:this.$ = new yy.ContentNode($$[$0]); 726 | break; 727 | case 16:this.$ = new yy.CommentNode($$[$0]); 728 | break; 729 | case 17:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); 730 | break; 731 | case 18:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); 732 | break; 733 | case 19:this.$ = {path: $$[$0-1], strip: stripFlags($$[$0-2], $$[$0])}; 734 | break; 735 | case 20:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); 736 | break; 737 | case 21:this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], $$[$0-2], stripFlags($$[$0-2], $$[$0])); 738 | break; 739 | case 22:this.$ = new yy.PartialNode($$[$0-2], $$[$0-1], stripFlags($$[$0-3], $$[$0])); 740 | break; 741 | case 23:this.$ = stripFlags($$[$0-1], $$[$0]); 742 | break; 743 | case 24:this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]]; 744 | break; 745 | case 25:this.$ = [[$$[$0]], null]; 746 | break; 747 | case 26:this.$ = $$[$0]; 748 | break; 749 | case 27:this.$ = new yy.StringNode($$[$0]); 750 | break; 751 | case 28:this.$ = new yy.IntegerNode($$[$0]); 752 | break; 753 | case 29:this.$ = new yy.BooleanNode($$[$0]); 754 | break; 755 | case 30:this.$ = $$[$0]; 756 | break; 757 | case 31:this.$ = new yy.HashNode($$[$0]); 758 | break; 759 | case 32:this.$ = [$$[$0-2], $$[$0]]; 760 | break; 761 | case 33:this.$ = new yy.PartialNameNode($$[$0]); 762 | break; 763 | case 34:this.$ = new yy.PartialNameNode(new yy.StringNode($$[$0])); 764 | break; 765 | case 35:this.$ = new yy.PartialNameNode(new yy.IntegerNode($$[$0])); 766 | break; 767 | case 36:this.$ = new yy.DataNode($$[$0]); 768 | break; 769 | case 37:this.$ = new yy.IdNode($$[$0]); 770 | break; 771 | case 38: $$[$0-2].push({part: $$[$0], separator: $$[$0-1]}); this.$ = $$[$0-2]; 772 | break; 773 | case 39:this.$ = [{part: $$[$0]}]; 774 | break; 775 | case 42:this.$ = []; 776 | break; 777 | case 43:$$[$0-1].push($$[$0]); 778 | break; 779 | case 46:this.$ = [$$[$0]]; 780 | break; 781 | case 47:$$[$0-1].push($$[$0]); 782 | break; 783 | } 784 | }, 785 | table: [{3:1,4:2,5:[1,3],8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[3]},{5:[1,16],8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],25:[1,15]},{1:[2,2]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],25:[2,9]},{4:20,6:18,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{4:20,6:22,7:19,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,8],22:[1,13],23:[1,14],25:[1,15]},{5:[2,13],14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],25:[2,13]},{5:[2,14],14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],25:[2,14]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],25:[2,15]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],25:[2,16]},{17:23,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:29,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:30,21:24,30:25,38:[1,28],40:[1,27],41:26},{17:31,21:24,30:25,38:[1,28],40:[1,27],41:26},{21:33,26:32,32:[1,34],33:[1,35],38:[1,28],41:26},{1:[2,1]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],25:[2,10]},{10:36,20:[1,37]},{4:38,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,7],22:[1,13],23:[1,14],25:[1,15]},{7:39,8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,21],20:[2,6],22:[1,13],23:[1,14],25:[1,15]},{17:23,18:[1,40],21:24,30:25,38:[1,28],40:[1,27],41:26},{10:41,20:[1,37]},{18:[1,42]},{18:[2,42],24:[2,42],28:43,32:[2,42],33:[2,42],34:[2,42],38:[2,42],40:[2,42]},{18:[2,25],24:[2,25]},{18:[2,37],24:[2,37],32:[2,37],33:[2,37],34:[2,37],38:[2,37],40:[2,37],42:[1,44]},{21:45,38:[1,28],41:26},{18:[2,39],24:[2,39],32:[2,39],33:[2,39],34:[2,39],38:[2,39],40:[2,39],42:[2,39]},{18:[1,46]},{18:[1,47]},{24:[1,48]},{18:[2,40],21:50,27:49,38:[1,28],41:26},{18:[2,33],38:[2,33]},{18:[2,34],38:[2,34]},{18:[2,35],38:[2,35]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],25:[2,11]},{21:51,38:[1,28],41:26},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,3],22:[1,13],23:[1,14],25:[1,15]},{4:52,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,5],22:[1,13],23:[1,14],25:[1,15]},{14:[2,23],15:[2,23],16:[2,23],19:[2,23],20:[2,23],22:[2,23],23:[2,23],25:[2,23]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],25:[2,12]},{14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],25:[2,18]},{18:[2,44],21:56,24:[2,44],29:53,30:60,31:54,32:[1,57],33:[1,58],34:[1,59],35:55,36:61,37:62,38:[1,63],40:[1,27],41:26},{38:[1,64]},{18:[2,36],24:[2,36],32:[2,36],33:[2,36],34:[2,36],38:[2,36],40:[2,36]},{14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],25:[2,17]},{5:[2,20],14:[2,20],15:[2,20],16:[2,20],19:[2,20],20:[2,20],22:[2,20],23:[2,20],25:[2,20]},{5:[2,21],14:[2,21],15:[2,21],16:[2,21],19:[2,21],20:[2,21],22:[2,21],23:[2,21],25:[2,21]},{18:[1,65]},{18:[2,41]},{18:[1,66]},{8:17,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],25:[1,15]},{18:[2,24],24:[2,24]},{18:[2,43],24:[2,43],32:[2,43],33:[2,43],34:[2,43],38:[2,43],40:[2,43]},{18:[2,45],24:[2,45]},{18:[2,26],24:[2,26],32:[2,26],33:[2,26],34:[2,26],38:[2,26],40:[2,26]},{18:[2,27],24:[2,27],32:[2,27],33:[2,27],34:[2,27],38:[2,27],40:[2,27]},{18:[2,28],24:[2,28],32:[2,28],33:[2,28],34:[2,28],38:[2,28],40:[2,28]},{18:[2,29],24:[2,29],32:[2,29],33:[2,29],34:[2,29],38:[2,29],40:[2,29]},{18:[2,30],24:[2,30],32:[2,30],33:[2,30],34:[2,30],38:[2,30],40:[2,30]},{18:[2,31],24:[2,31],37:67,38:[1,68]},{18:[2,46],24:[2,46],38:[2,46]},{18:[2,39],24:[2,39],32:[2,39],33:[2,39],34:[2,39],38:[2,39],39:[1,69],40:[2,39],42:[2,39]},{18:[2,38],24:[2,38],32:[2,38],33:[2,38],34:[2,38],38:[2,38],40:[2,38],42:[2,38]},{5:[2,22],14:[2,22],15:[2,22],16:[2,22],19:[2,22],20:[2,22],22:[2,22],23:[2,22],25:[2,22]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],25:[2,19]},{18:[2,47],24:[2,47],38:[2,47]},{39:[1,69]},{21:56,30:60,31:70,32:[1,57],33:[1,58],34:[1,59],38:[1,28],40:[1,27],41:26},{18:[2,32],24:[2,32],38:[2,32]}], 786 | defaultActions: {3:[2,2],16:[2,1],50:[2,41]}, 787 | parseError: function parseError(str, hash) { 788 | throw new Error(str); 789 | }, 790 | parse: function parse(input) { 791 | var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; 792 | this.lexer.setInput(input); 793 | this.lexer.yy = this.yy; 794 | this.yy.lexer = this.lexer; 795 | this.yy.parser = this; 796 | if (typeof this.lexer.yylloc == "undefined") 797 | this.lexer.yylloc = {}; 798 | var yyloc = this.lexer.yylloc; 799 | lstack.push(yyloc); 800 | var ranges = this.lexer.options && this.lexer.options.ranges; 801 | if (typeof this.yy.parseError === "function") 802 | this.parseError = this.yy.parseError; 803 | function popStack(n) { 804 | stack.length = stack.length - 2 * n; 805 | vstack.length = vstack.length - n; 806 | lstack.length = lstack.length - n; 807 | } 808 | function lex() { 809 | var token; 810 | token = self.lexer.lex() || 1; 811 | if (typeof token !== "number") { 812 | token = self.symbols_[token] || token; 813 | } 814 | return token; 815 | } 816 | var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; 817 | while (true) { 818 | state = stack[stack.length - 1]; 819 | if (this.defaultActions[state]) { 820 | action = this.defaultActions[state]; 821 | } else { 822 | if (symbol === null || typeof symbol == "undefined") { 823 | symbol = lex(); 824 | } 825 | action = table[state] && table[state][symbol]; 826 | } 827 | if (typeof action === "undefined" || !action.length || !action[0]) { 828 | var errStr = ""; 829 | if (!recovering) { 830 | expected = []; 831 | for (p in table[state]) 832 | if (this.terminals_[p] && p > 2) { 833 | expected.push("'" + this.terminals_[p] + "'"); 834 | } 835 | if (this.lexer.showPosition) { 836 | errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; 837 | } else { 838 | errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); 839 | } 840 | this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 841 | } 842 | } 843 | if (action[0] instanceof Array && action.length > 1) { 844 | throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); 845 | } 846 | switch (action[0]) { 847 | case 1: 848 | stack.push(symbol); 849 | vstack.push(this.lexer.yytext); 850 | lstack.push(this.lexer.yylloc); 851 | stack.push(action[1]); 852 | symbol = null; 853 | if (!preErrorSymbol) { 854 | yyleng = this.lexer.yyleng; 855 | yytext = this.lexer.yytext; 856 | yylineno = this.lexer.yylineno; 857 | yyloc = this.lexer.yylloc; 858 | if (recovering > 0) 859 | recovering--; 860 | } else { 861 | symbol = preErrorSymbol; 862 | preErrorSymbol = null; 863 | } 864 | break; 865 | case 2: 866 | len = this.productions_[action[1]][1]; 867 | yyval.$ = vstack[vstack.length - len]; 868 | yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; 869 | if (ranges) { 870 | yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; 871 | } 872 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 873 | if (typeof r !== "undefined") { 874 | return r; 875 | } 876 | if (len) { 877 | stack = stack.slice(0, -1 * len * 2); 878 | vstack = vstack.slice(0, -1 * len); 879 | lstack = lstack.slice(0, -1 * len); 880 | } 881 | stack.push(this.productions_[action[1]][0]); 882 | vstack.push(yyval.$); 883 | lstack.push(yyval._$); 884 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 885 | stack.push(newState); 886 | break; 887 | case 3: 888 | return true; 889 | } 890 | } 891 | return true; 892 | } 893 | }; 894 | 895 | 896 | function stripFlags(open, close) { 897 | return { 898 | left: open.charAt(2) === '~', 899 | right: close.charAt(0) === '~' || close.charAt(1) === '~' 900 | }; 901 | } 902 | 903 | /* Jison generated lexer */ 904 | var lexer = (function(){ 905 | var lexer = ({EOF:1, 906 | parseError:function parseError(str, hash) { 907 | if (this.yy.parser) { 908 | this.yy.parser.parseError(str, hash); 909 | } else { 910 | throw new Error(str); 911 | } 912 | }, 913 | setInput:function (input) { 914 | this._input = input; 915 | this._more = this._less = this.done = false; 916 | this.yylineno = this.yyleng = 0; 917 | this.yytext = this.matched = this.match = ''; 918 | this.conditionStack = ['INITIAL']; 919 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 920 | if (this.options.ranges) this.yylloc.range = [0,0]; 921 | this.offset = 0; 922 | return this; 923 | }, 924 | input:function () { 925 | var ch = this._input[0]; 926 | this.yytext += ch; 927 | this.yyleng++; 928 | this.offset++; 929 | this.match += ch; 930 | this.matched += ch; 931 | var lines = ch.match(/(?:\r\n?|\n).*/g); 932 | if (lines) { 933 | this.yylineno++; 934 | this.yylloc.last_line++; 935 | } else { 936 | this.yylloc.last_column++; 937 | } 938 | if (this.options.ranges) this.yylloc.range[1]++; 939 | 940 | this._input = this._input.slice(1); 941 | return ch; 942 | }, 943 | unput:function (ch) { 944 | var len = ch.length; 945 | var lines = ch.split(/(?:\r\n?|\n)/g); 946 | 947 | this._input = ch + this._input; 948 | this.yytext = this.yytext.substr(0, this.yytext.length-len-1); 949 | //this.yyleng -= len; 950 | this.offset -= len; 951 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 952 | this.match = this.match.substr(0, this.match.length-1); 953 | this.matched = this.matched.substr(0, this.matched.length-1); 954 | 955 | if (lines.length-1) this.yylineno -= lines.length-1; 956 | var r = this.yylloc.range; 957 | 958 | this.yylloc = {first_line: this.yylloc.first_line, 959 | last_line: this.yylineno+1, 960 | first_column: this.yylloc.first_column, 961 | last_column: lines ? 962 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: 963 | this.yylloc.first_column - len 964 | }; 965 | 966 | if (this.options.ranges) { 967 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 968 | } 969 | return this; 970 | }, 971 | more:function () { 972 | this._more = true; 973 | return this; 974 | }, 975 | less:function (n) { 976 | this.unput(this.match.slice(n)); 977 | }, 978 | pastInput:function () { 979 | var past = this.matched.substr(0, this.matched.length - this.match.length); 980 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 981 | }, 982 | upcomingInput:function () { 983 | var next = this.match; 984 | if (next.length < 20) { 985 | next += this._input.substr(0, 20-next.length); 986 | } 987 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 988 | }, 989 | showPosition:function () { 990 | var pre = this.pastInput(); 991 | var c = new Array(pre.length + 1).join("-"); 992 | return pre + this.upcomingInput() + "\n" + c+"^"; 993 | }, 994 | next:function () { 995 | if (this.done) { 996 | return this.EOF; 997 | } 998 | if (!this._input) this.done = true; 999 | 1000 | var token, 1001 | match, 1002 | tempMatch, 1003 | index, 1004 | col, 1005 | lines; 1006 | if (!this._more) { 1007 | this.yytext = ''; 1008 | this.match = ''; 1009 | } 1010 | var rules = this._currentRules(); 1011 | for (var i=0;i < rules.length; i++) { 1012 | tempMatch = this._input.match(this.rules[rules[i]]); 1013 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 1014 | match = tempMatch; 1015 | index = i; 1016 | if (!this.options.flex) break; 1017 | } 1018 | } 1019 | if (match) { 1020 | lines = match[0].match(/(?:\r\n?|\n).*/g); 1021 | if (lines) this.yylineno += lines.length; 1022 | this.yylloc = {first_line: this.yylloc.last_line, 1023 | last_line: this.yylineno+1, 1024 | first_column: this.yylloc.last_column, 1025 | last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; 1026 | this.yytext += match[0]; 1027 | this.match += match[0]; 1028 | this.matches = match; 1029 | this.yyleng = this.yytext.length; 1030 | if (this.options.ranges) { 1031 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 1032 | } 1033 | this._more = false; 1034 | this._input = this._input.slice(match[0].length); 1035 | this.matched += match[0]; 1036 | token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); 1037 | if (this.done && this._input) this.done = false; 1038 | if (token) return token; 1039 | else return; 1040 | } 1041 | if (this._input === "") { 1042 | return this.EOF; 1043 | } else { 1044 | return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 1045 | {text: "", token: null, line: this.yylineno}); 1046 | } 1047 | }, 1048 | lex:function lex() { 1049 | var r = this.next(); 1050 | if (typeof r !== 'undefined') { 1051 | return r; 1052 | } else { 1053 | return this.lex(); 1054 | } 1055 | }, 1056 | begin:function begin(condition) { 1057 | this.conditionStack.push(condition); 1058 | }, 1059 | popState:function popState() { 1060 | return this.conditionStack.pop(); 1061 | }, 1062 | _currentRules:function _currentRules() { 1063 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 1064 | }, 1065 | topState:function () { 1066 | return this.conditionStack[this.conditionStack.length-2]; 1067 | }, 1068 | pushState:function begin(condition) { 1069 | this.begin(condition); 1070 | }}); 1071 | lexer.options = {}; 1072 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 1073 | 1074 | 1075 | function strip(start, end) { 1076 | return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng-end); 1077 | } 1078 | 1079 | 1080 | var YYSTATE=YY_START 1081 | switch($avoiding_name_collisions) { 1082 | case 0: 1083 | if(yy_.yytext.slice(-2) === "\\\\") { 1084 | strip(0,1); 1085 | this.begin("mu"); 1086 | } else if(yy_.yytext.slice(-1) === "\\") { 1087 | strip(0,1); 1088 | this.begin("emu"); 1089 | } else { 1090 | this.begin("mu"); 1091 | } 1092 | if(yy_.yytext) return 14; 1093 | 1094 | break; 1095 | case 1:return 14; 1096 | break; 1097 | case 2: 1098 | this.popState(); 1099 | return 14; 1100 | 1101 | break; 1102 | case 3:strip(0,4); this.popState(); return 15; 1103 | break; 1104 | case 4:return 25; 1105 | break; 1106 | case 5:return 16; 1107 | break; 1108 | case 6:return 20; 1109 | break; 1110 | case 7:return 19; 1111 | break; 1112 | case 8:return 19; 1113 | break; 1114 | case 9:return 23; 1115 | break; 1116 | case 10:return 22; 1117 | break; 1118 | case 11:this.popState(); this.begin('com'); 1119 | break; 1120 | case 12:strip(3,5); this.popState(); return 15; 1121 | break; 1122 | case 13:return 22; 1123 | break; 1124 | case 14:return 39; 1125 | break; 1126 | case 15:return 38; 1127 | break; 1128 | case 16:return 38; 1129 | break; 1130 | case 17:return 42; 1131 | break; 1132 | case 18:// ignore whitespace 1133 | break; 1134 | case 19:this.popState(); return 24; 1135 | break; 1136 | case 20:this.popState(); return 18; 1137 | break; 1138 | case 21:yy_.yytext = strip(1,2).replace(/\\"/g,'"'); return 32; 1139 | break; 1140 | case 22:yy_.yytext = strip(1,2).replace(/\\'/g,"'"); return 32; 1141 | break; 1142 | case 23:return 40; 1143 | break; 1144 | case 24:return 34; 1145 | break; 1146 | case 25:return 34; 1147 | break; 1148 | case 26:return 33; 1149 | break; 1150 | case 27:return 38; 1151 | break; 1152 | case 28:yy_.yytext = strip(1,2); return 38; 1153 | break; 1154 | case 29:return 'INVALID'; 1155 | break; 1156 | case 30:return 5; 1157 | break; 1158 | } 1159 | }; 1160 | lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{(~)?>)/,/^(?:\{\{(~)?#)/,/^(?:\{\{(~)?\/)/,/^(?:\{\{(~)?\^)/,/^(?:\{\{(~)?\s*else\b)/,/^(?:\{\{(~)?\{)/,/^(?:\{\{(~)?&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{(~)?)/,/^(?:=)/,/^(?:\.\.)/,/^(?:\.(?=([=~}\s\/.])))/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}(~)?\}\})/,/^(?:(~)?\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=([~}\s])))/,/^(?:false(?=([~}\s])))/,/^(?:-?[0-9]+(?=([~}\s])))/,/^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.]))))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; 1161 | lexer.conditions = {"mu":{"rules":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"com":{"rules":[3],"inclusive":false},"INITIAL":{"rules":[0,1,30],"inclusive":true}}; 1162 | return lexer;})() 1163 | parser.lexer = lexer; 1164 | function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; 1165 | return new Parser; 1166 | })();__exports__ = handlebars; 1167 | /* jshint ignore:end */ 1168 | return __exports__; 1169 | })(); 1170 | 1171 | // handlebars/compiler/base.js 1172 | var __module8__ = (function(__dependency1__, __dependency2__) { 1173 | "use strict"; 1174 | var __exports__ = {}; 1175 | var parser = __dependency1__; 1176 | var AST = __dependency2__; 1177 | 1178 | __exports__.parser = parser; 1179 | 1180 | function parse(input) { 1181 | // Just return if an already-compile AST was passed in. 1182 | if(input.constructor === AST.ProgramNode) { return input; } 1183 | 1184 | parser.yy = AST; 1185 | return parser.parse(input); 1186 | } 1187 | 1188 | __exports__.parse = parse; 1189 | return __exports__; 1190 | })(__module9__, __module7__); 1191 | 1192 | // handlebars/compiler/javascript-compiler.js 1193 | var __module11__ = (function(__dependency1__) { 1194 | "use strict"; 1195 | var __exports__; 1196 | var COMPILER_REVISION = __dependency1__.COMPILER_REVISION; 1197 | var REVISION_CHANGES = __dependency1__.REVISION_CHANGES; 1198 | var log = __dependency1__.log; 1199 | 1200 | function Literal(value) { 1201 | this.value = value; 1202 | } 1203 | 1204 | function JavaScriptCompiler() {} 1205 | 1206 | JavaScriptCompiler.prototype = { 1207 | // PUBLIC API: You can override these methods in a subclass to provide 1208 | // alternative compiled forms for name lookup and buffering semantics 1209 | nameLookup: function(parent, name /* , type*/) { 1210 | var wrap, 1211 | ret; 1212 | if (parent.indexOf('depth') === 0) { 1213 | wrap = true; 1214 | } 1215 | 1216 | if (/^[0-9]+$/.test(name)) { 1217 | ret = parent + "[" + name + "]"; 1218 | } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { 1219 | ret = parent + "." + name; 1220 | } 1221 | else { 1222 | ret = parent + "['" + name + "']"; 1223 | } 1224 | 1225 | if (wrap) { 1226 | return '(' + parent + ' && ' + ret + ')'; 1227 | } else { 1228 | return ret; 1229 | } 1230 | }, 1231 | 1232 | compilerInfo: function() { 1233 | var revision = COMPILER_REVISION, 1234 | versions = REVISION_CHANGES[revision]; 1235 | return "this.compilerInfo = ["+revision+",'"+versions+"'];\n"; 1236 | }, 1237 | 1238 | appendToBuffer: function(string) { 1239 | if (this.environment.isSimple) { 1240 | return "return " + string + ";"; 1241 | } else { 1242 | return { 1243 | appendToBuffer: true, 1244 | content: string, 1245 | toString: function() { return "buffer += " + string + ";"; } 1246 | }; 1247 | } 1248 | }, 1249 | 1250 | initializeBuffer: function() { 1251 | return this.quotedString(""); 1252 | }, 1253 | 1254 | namespace: "Handlebars", 1255 | // END PUBLIC API 1256 | 1257 | compile: function(environment, options, context, asObject) { 1258 | this.environment = environment; 1259 | this.options = options || {}; 1260 | 1261 | log('debug', this.environment.disassemble() + "\n\n"); 1262 | 1263 | this.name = this.environment.name; 1264 | this.isChild = !!context; 1265 | this.context = context || { 1266 | programs: [], 1267 | environments: [], 1268 | aliases: { } 1269 | }; 1270 | 1271 | this.preamble(); 1272 | 1273 | this.stackSlot = 0; 1274 | this.stackVars = []; 1275 | this.registers = { list: [] }; 1276 | this.compileStack = []; 1277 | this.inlineStack = []; 1278 | 1279 | this.compileChildren(environment, options); 1280 | 1281 | var opcodes = environment.opcodes, opcode; 1282 | 1283 | this.i = 0; 1284 | 1285 | for(var l=opcodes.length; this.i 0) { 1336 | this.source[1] = this.source[1] + ", " + locals.join(", "); 1337 | } 1338 | 1339 | // Generate minimizer alias mappings 1340 | if (!this.isChild) { 1341 | for (var alias in this.context.aliases) { 1342 | if (this.context.aliases.hasOwnProperty(alias)) { 1343 | this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; 1344 | } 1345 | } 1346 | } 1347 | 1348 | if (this.source[1]) { 1349 | this.source[1] = "var " + this.source[1].substring(2) + ";"; 1350 | } 1351 | 1352 | // Merge children 1353 | if (!this.isChild) { 1354 | this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; 1355 | } 1356 | 1357 | if (!this.environment.isSimple) { 1358 | this.pushSource("return buffer;"); 1359 | } 1360 | 1361 | var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; 1362 | 1363 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1927 | return this.topStackName(); 1928 | }, 1929 | topStackName: function() { 1930 | return "stack" + this.stackSlot; 1931 | }, 1932 | flushInline: function() { 1933 | var inlineStack = this.inlineStack; 1934 | if (inlineStack.length) { 1935 | this.inlineStack = []; 1936 | for (var i = 0, len = inlineStack.length; i < len; i++) { 1937 | var entry = inlineStack[i]; 1938 | if (entry instanceof Literal) { 1939 | this.compileStack.push(entry); 1940 | } else { 1941 | this.pushStack(entry); 1942 | } 1943 | } 1944 | } 1945 | }, 1946 | isInline: function() { 1947 | return this.inlineStack.length; 1948 | }, 1949 | 1950 | popStack: function(wrapped) { 1951 | var inline = this.isInline(), 1952 | item = (inline ? this.inlineStack : this.compileStack).pop(); 1953 | 1954 | if (!wrapped && (item instanceof Literal)) { 1955 | return item.value; 1956 | } else { 1957 | if (!inline) { 1958 | this.stackSlot--; 1959 | } 1960 | return item; 1961 | } 1962 | }, 1963 | 1964 | topStack: function(wrapped) { 1965 | var stack = (this.isInline() ? this.inlineStack : this.compileStack), 1966 | item = stack[stack.length - 1]; 1967 | 1968 | if (!wrapped && (item instanceof Literal)) { 1969 | return item.value; 1970 | } else { 1971 | return item; 1972 | } 1973 | }, 1974 | 1975 | quotedString: function(str) { 1976 | return '"' + str 1977 | .replace(/\\/g, '\\\\') 1978 | .replace(/"/g, '\\"') 1979 | .replace(/\n/g, '\\n') 1980 | .replace(/\r/g, '\\r') 1981 | .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 1982 | .replace(/\u2029/g, '\\u2029') + '"'; 1983 | }, 1984 | 1985 | setupHelper: function(paramSize, name, missingParams) { 1986 | var params = []; 1987 | this.setupParams(paramSize, params, missingParams); 1988 | var foundHelper = this.nameLookup('helpers', name, 'helper'); 1989 | 1990 | return { 1991 | params: params, 1992 | name: foundHelper, 1993 | callParams: ["depth0"].concat(params).join(", "), 1994 | helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") 1995 | }; 1996 | }, 1997 | 1998 | // the params and contexts arguments are passed in arrays 1999 | // to fill in 2000 | setupParams: function(paramSize, params, useRegister) { 2001 | var options = [], contexts = [], types = [], param, inverse, program; 2002 | 2003 | options.push("hash:" + this.popStack()); 2004 | 2005 | inverse = this.popStack(); 2006 | program = this.popStack(); 2007 | 2008 | // Avoid setting fn and inverse if neither are set. This allows 2009 | // helpers to do a check for `if (options.fn)` 2010 | if (program || inverse) { 2011 | if (!program) { 2012 | this.context.aliases.self = "this"; 2013 | program = "self.noop"; 2014 | } 2015 | 2016 | if (!inverse) { 2017 | this.context.aliases.self = "this"; 2018 | inverse = "self.noop"; 2019 | } 2020 | 2021 | options.push("inverse:" + inverse); 2022 | options.push("fn:" + program); 2023 | } 2024 | 2025 | for(var i=0; i