├── 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 |
"
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 | | {{session.name}} |
5 |
6 |
7 | | {{session.room}} |
8 |
9 |
10 | {{#each speaker in session.speakers}}
11 | | {{speaker.name}} |
12 | {{/each}}
13 |
14 |
15 | {{#each rating in session.ratings}}
16 | | {{rating.score}} |
17 | {{/each}}
18 |
19 |
20 | {{#each tag in session.tags}}
21 | | {{tag.description}} |
22 | {{/each}}
23 |
24 |
25 | | {{#link-to 'session' session}}View Session Details{{/link-to}} |
26 |
27 | {{/each}}
28 |
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 | {{rating.feedback}}
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 | {{rating.feedback}}
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