├── .circleci └── config.yml ├── .editorconfig ├── .ember-cli ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .watchmanconfig ├── AUTHORS.md ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── addon ├── adapters │ └── drf.js └── serializers │ └── drf.js ├── app ├── .gitkeep ├── adapters │ ├── application.js │ └── drf.js └── serializers │ ├── application.js │ └── drf.js ├── blueprints ├── drf-adapter │ ├── files │ │ └── app │ │ │ └── adapters │ │ │ └── __name__.js │ └── index.js └── drf-serializer │ ├── files │ └── app │ │ └── serializers │ │ └── __name__.js │ └── index.js ├── config ├── ember-try.js └── environment.js ├── docs ├── coalesce-find-requests.md ├── configuring.md ├── contributing.md ├── embedded-records.md ├── extending.md ├── hyperlinked-related-fields.md ├── index.md ├── non-field-errors.md ├── pagination.md └── trailing-slashes.md ├── ember-cli-build.js ├── index.js ├── mkdocs.yml ├── package-lock.json ├── package.json ├── testem.js ├── tests ├── acceptance │ ├── crud-failure-test.js │ ├── crud-success-test.js │ ├── embedded-records-test.js │ ├── pagination-test.js │ ├── relationship-links-test.js │ └── relationships-test.js ├── dummy │ ├── .jshintrc │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ ├── comment.js │ │ │ ├── embedded-comments-post.js │ │ │ ├── embedded-post-comment.js │ │ │ └── post.js │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── serializers │ │ │ ├── embedded-comments-post.js │ │ │ └── embedded-post-comment.js │ │ ├── styles │ │ │ ├── .gitkeep │ │ │ └── app.css │ │ ├── templates │ │ │ ├── .gitkeep │ │ │ ├── application.hbs │ │ │ └── components │ │ │ │ └── .gitkeep │ │ └── views │ │ │ └── .gitkeep │ ├── config │ │ ├── environment.js │ │ └── targets.js │ └── public │ │ ├── .gitkeep │ │ └── robots.txt ├── helpers │ └── .gitkeep ├── index.html ├── test-helper.js └── unit │ ├── .gitkeep │ ├── adapters │ └── drf-test.js │ └── serializers │ └── drf-test.js ├── vendor ├── .gitkeep └── ember-django-adapter │ └── register-version.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/node:8-browsers 6 | steps: 7 | - checkout 8 | - run: yarn install 9 | - run: yarn test 10 | 11 | deploy: 12 | docker: 13 | - image: circleci/python:3.6.5 14 | working_directory: ~/circleci-docs-deploy 15 | steps: 16 | - add_ssh_keys: 17 | fingerprints: 18 | - "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00" 19 | - checkout 20 | - run: sudo pip install mkdocs 21 | - run: "mkdocs gh-deploy --message=\"Deployed {sha} with MkDocs version: {version} [ci skip]\"" 22 | 23 | workflows: 24 | version: 2 25 | test_and_deploy: 26 | jobs: 27 | - test: 28 | filters: # required since `deploy` has tag filters AND requires `test` 29 | tags: 30 | only: /.*/ 31 | - deploy: 32 | requires: 33 | - test 34 | filters: 35 | tags: 36 | only: /^v.*/ 37 | branches: 38 | ignore: /.*/ 39 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | plugins: [ 8 | 'ember' 9 | ], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:ember/recommended' 13 | ], 14 | env: { 15 | browser: true 16 | }, 17 | rules: { 18 | }, 19 | overrides: [ 20 | // node files 21 | { 22 | files: [ 23 | 'ember-cli-build.js', 24 | 'index.js', 25 | 'testem.js', 26 | 'config/**/*.js', 27 | 'tests/dummy/config/**/*.js' 28 | ], 29 | excludedFiles: [ 30 | 'addon/**', 31 | 'addon-test-support/**', 32 | 'app/**', 33 | 'tests/dummy/app/**' 34 | ], 35 | parserOptions: { 36 | sourceType: 'script', 37 | ecmaVersion: 2015 38 | }, 39 | env: { 40 | browser: false, 41 | node: true 42 | }, 43 | plugins: ['node'], 44 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 45 | // add your custom rules and overrides for node files here 46 | }) 47 | } 48 | ] 49 | }; 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.travis.yml 13 | /.idea 14 | /.sass-cache 15 | /connect.lock 16 | /coverage/* 17 | /libpeerconnection.log 18 | npm-debug.log* 19 | yarn-error.log 20 | testem.log 21 | 22 | # ember-try 23 | .node_modules.ember-try/ 24 | bower.json.ember-try 25 | package.json.ember-try 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .gitignore 11 | .eslintrc.js 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | 18 | # ember-try 19 | .node_modules.ember-try/ 20 | bower.json.ember-try 21 | package.json.ember-try 22 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | * Ben Konrath ([@benkonrath](https://github.com/benkonrath)) 5 | * Dustin Farris ([@dustinfarris](https://github.com/dustinfarris)) 6 | * Pablo Klijnjan ([@holandes22](https://github.com/holandes22)) 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ember-django-adapter Changelog 2 | ============================== 3 | 4 | 2.1.1 5 | ----- 6 | 7 | ### Internal 8 | 9 | * [#225](https://github.com/dustinfarris/ember-django-adapter/pull/225): Update error class imports (@gojefferson) 10 | * [#220](https://github.com/dustinfarris/ember-django-adapter/pull/220): Update adapter errors imports & make ember-data a peer dependency (@gojefferson) 11 | 12 | 13 | 2.1.0 14 | ----- 15 | 16 | ### Enhancements 17 | 18 | * [#215](https://github.com/dustinfarris/ember-django-adapter/pull/215): Support ember-data 3.0 (@benmurden, @OleRoel) 19 | 20 | 21 | ### Internal 22 | 23 | * [#217](https://github.com/dustinfarris/ember-django-adapter/pull/217): Upgrade to CircleCI 2.0 (@benmurden) 24 | 25 | 26 | ### Docs 27 | 28 | * [#209](https://github.com/dustinfarris/ember-django-adapter/pull/209): Update coalesce-find-requests code sample (@arnebit) 29 | 30 | 31 | 2.0.0 32 | ----- 33 | 34 | ### Enhancements 35 | 36 | * [#207](https://github.com/dustinfarris/ember-django-adapter/pull/207): Update ember and ember-data to 2.17. Fix deprecations. 37 | * [#172](https://github.com/dustinfarris/ember-django-adapter/pull/172): clean Ember.merge deprecation and fix jshintrc 38 | 39 | 40 | ### Internal 41 | 42 | * [#186](https://github.com/dustinfarris/ember-django-adapter/pull/186): Update ember-sinon to the latest version 43 | * [#180](https://github.com/dustinfarris/ember-django-adapter/pull/180): Update ember-resolver to the latest version 44 | * [#174](https://github.com/dustinfarris/ember-django-adapter/pull/174): Update ember-cli-jshint to the latest version 45 | * [#167](https://github.com/dustinfarris/ember-django-adapter/pull/167): Update dependencies to enable Greenkeeper 46 | 47 | 48 | ### Docs 49 | 50 | * [#175](https://github.com/dustinfarris/ember-django-adapter/pull/175): Update pagination.md 51 | 52 | 53 | 1.1.3 54 | ----- 55 | 56 | * [INTERNAL] Upgrade ember-cli to 1.13.15 ([#157](https://github.com/dustinfarris/ember-django-adapter/pull/157)) 57 | * [ENHANCEMENT] Allow addon to be used in a another addon ([#164](https://github.com/dustinfarris/ember-django-adapter/pull/164)) 58 | * [BUGFIX] Map payload strings to arrays ([#165](https://github.com/dustinfarris/ember-django-adapter/pull/165)) 59 | 60 | 61 | 1.1.2 62 | ----- 63 | 64 | * [ENHANCMENT] Register addon with Ember libraries ([#142](https://github.com/dustinfarris/ember-django-adapter/pull/142)) 65 | * [BUGFIX] Do not check for count attribute in paginated response ([#143](https://github.com/dustinfarris/ember-django-adapter/pull/143)) 66 | * [DOCS] Note to disable pagination for coalesced records ([#145](https://github.com/dustinfarris/ember-django-adapter/pull/145)) 67 | 68 | 69 | 1.1.1 70 | ----- 71 | 72 | * [BUGFIX] Support nested errors returned by DRF ([#141](https://github.com/dustinfarris/ember-django-adapter/pull/141)) 73 | * [BUGFIX] Do not require page query param on pagination previous link ([#140](https://github.com/dustinfarris/ember-django-adapter/pull/140)) 74 | * [INTERNAL] Upgrade ember and ember-cli to latest ([#138](https://github.com/dustinfarris/ember-django-adapter/pull/138)) 75 | 76 | 77 | 1.1.0 78 | ----- 79 | 80 | * [ENHANCEMENT] Extend DS.RESTSerializer ([#133](https://github.com/dustinfarris/ember-django-adapter/pull/133)) 81 | 82 | 83 | 1.0.0 84 | ----- 85 | 86 | * [BREAKING ENHANCEMENT] Update to new Ember Data 1.13 serializer API ([#114](https://github.com/dustinfarris/ember-django-adapter/pull/114)) 87 | * [ENHANCEMENT] Support ember-data 1.13 series ([#108](https://github.com/dustinfarris/ember-django-adapter/pull/108)) 88 | * [ENHANCEMENT] Support HyperlinkedRelatedFields ([#95](https://github.com/dustinfarris/ember-django-adapter/pull/95)) 89 | * [ENHANCEMENT] Support object-level errors ([#123](https://github.com/dustinfarris/ember-django-adapter/pull/123)) 90 | * [ENHANCEMENT] Support query parameter in buildURL ([#124](https://github.com/dustinfarris/ember-django-adapter/pull/124)) 91 | * [BUGFIX] Remove coalesceFindRequests warning ([#106](https://github.com/dustinfarris/ember-django-adapter/pull/106)) 92 | * [INTERNAL] Updated ember-cli version to latest (1.13.1) ([#112](https://github.com/dustinfarris/ember-django-adapter/pull/112)) 93 | * [INTERNAL] Test for setting an explicit id on createRecord ([#117](https://github.com/dustinfarris/ember-django-adapter/pull/117)) 94 | * [INTERNAL] Acceptance test for embedded records ([#119](https://github.com/dustinfarris/ember-django-adapter/pull/119)) 95 | * [INTERNAL] Test for embedded belongsTo create with id ([#120](https://github.com/dustinfarris/ember-django-adapter/pull/120)) 96 | * [DOCS] Using ember-cli-pagination with the adapter ([#101](https://github.com/dustinfarris/ember-django-adapter/pull/101)) 97 | 98 | 99 | 0.5.6 100 | ----- 101 | 102 | * [INTERNAL] Updated ember-cli version to latest (0.2.7) ([#99](https://github.com/dustinfarris/ember-django-adapter/pull/99)) 103 | * [INTERNAL] Updated ember-cli version to latest (0.2.6) ([#97](https://github.com/dustinfarris/ember-django-adapter/pull/97)) 104 | * [ENHANCEMENT] Support ember-data 1.0.0-beta.18 ([#96](https://github.com/dustinfarris/ember-django-adapter/pull/96)) 105 | * [INTERNAL] Add tests for relationships support ([#94](https://github.com/dustinfarris/ember-django-adapter/pull/94)) 106 | * [INTERNAL] Updated ember-cli version to latest (0.2.5) ([#91](https://github.com/dustinfarris/ember-django-adapter/pull/91)) 107 | 108 | 109 | 0.5.5 110 | ----- 111 | 112 | * [INTERNAL] Updated ember-cli version to latest (0.2.4) ([#89](https://github.com/dustinfarris/ember-django-adapter/pull/89)) 113 | * [BUGFIX] All find queries are now handled properly ([#88](https://github.com/dustinfarris/ember-django-adapter/pull/88)) 114 | 115 | 116 | 0.5.4 117 | ----- 118 | 119 | * [INTERNAL] Updated ember-cli version to latest (0.2.3) ([#81](https://github.com/dustinfarris/ember-django-adapter/pull/81)) 120 | * [ENHANCEMENT] Modified signatures of methods in serializer and adapter 121 | to comply with changes introduced in ember-data v1.0.0-beta.15 and in 122 | v1.0.0-beta.16 (snapshots instead of records) ([#74](https://github.com/dustinfarris/ember-django-adapter/pull/74)) ([#77](https://github.com/dustinfarris/ember-django-adapter/pull/77)) ([#84](https://github.com/dustinfarris/ember-django-adapter/pull/84)) 123 | * [INTERNAL] Use ember-try to enable a test matrix ([#85](https://github.com/dustinfarris/ember-django-adapter/pull/85)) 124 | 125 | 126 | 0.5.3 127 | ----- 128 | 129 | * [BREAKING ENHANCEMENT] Remove trailing slashes environment config ([#67](https://github.com/dustinfarris/ember-django-adapter/pull/67)) 130 | * [DOCS] Add Google Analytics to documentation site ([#69](https://github.com/dustinfarris/ember-django-adapter/pull/69)) 131 | * [ENHANCEMENT] Support added for coalesceFindRequests ([#68](https://github.com/dustinfarris/ember-django-adapter/pull/68)) 132 | * [INTERNAL] Revised goals for the adapter ([#70](https://github.com/dustinfarris/ember-django-adapter/pull/70)) 133 | 134 | 135 | 0.5.2 136 | ----- 137 | 138 | * [BUGFIX] Return jqXHR for non-400 errors ([#62](https://github.com/dustinfarris/ember-django-adapter/pull/62)) 139 | * [BREAKING BUGFIX] Set default host to localhost:8000 ([#64](https://github.com/dustinfarris/ember-django-adapter/pull/64)) 140 | * [DOCS] Update installation instructions ([#65](https://github.com/dustinfarris/ember-django-adapter/pull/65)) 141 | 142 | 143 | 0.5.1 144 | ----- 145 | 146 | * [ENHANCEMENT] Add support for pagination metadata ([#45](https://github.com/dustinfarris/ember-django-adapter/pull/45)) 147 | * [ENHANCEMENT] Add documentation for contributing ([#49](https://github.com/dustinfarris/ember-django-adapter/pull/49)) 148 | * [ENHANCEMENT] Add blueprints and support for embedded records ([#51](https://github.com/dustinfarris/ember-django-adapter/pull/51)) 149 | * [ENHANCEMENT] Add option to remove trailing slashes ([#50](https://github.com/dustinfarris/ember-django-adapter/pull/50)) 150 | * [ENHANCEMENT] Test coverage for all supported versions of ember-data ([#56](https://github.com/dustinfarris/ember-django-adapter/pull/56)) 151 | 152 | 153 | 0.5.0 154 | ----- 155 | 156 | * [BREAKING REFACTOR] Rewrite [toranb/ember-data-django-rest-adapter][] as an ember-cli addon 157 | 158 | 159 | 160 | [toranb/ember-data-django-rest-adapter]: https://github.com/toranb/ember-data-django-rest-adapter 161 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Call for maintainers 2 | -------------------- 3 | 4 | This repo is several years out of date. If anyone is interested in modernizing 5 | the dependencies and code base, please let me know. 6 | 7 | Ember Django Adapter 8 | ==================== 9 | 10 | [![Circle CI](https://circleci.com/gh/dustinfarris/ember-django-adapter/tree/master.png?style=badge)](https://circleci.com/gh/dustinfarris/ember-django-adapter/tree/master) 11 | 12 | [![Ember Observer Score](http://emberobserver.com/badges/ember-django-adapter.svg)](http://emberobserver.com/addons/ember-django-adapter) 13 | 14 | [Ember Data][] is a core Ember.js library that provides a store and ORM for working 15 | with your Ember models. Ember Data works with JSON API out of the box, however 16 | "Ember Data is designed to be agnostic to the underlying persistence mechanism". 17 | To that end, Ember Data encourages the use of adapters to manage communication with 18 | various backend APIs. 19 | 20 | This adapter enables the use of [Django REST Framework][] as an API backend for 21 | Ember Data. The addon is compatible with [ember-cli][] version 0.2.7 and higher, 22 | Ember 1.12.1 and higher (including 2.0.0), and Ember Data v1.13.7 and higher 23 | (including 2.0.0). 24 | 25 | 26 | Community 27 | --------- 28 | 29 | * IRC: #ember-django-adapter on freenode 30 | * Issues: [ember-django-adapter/issues][] 31 | * Website: [dustinfarris.github.io/ember-django-adapter][] 32 | 33 | 34 | Development Hints 35 | ----------------- 36 | 37 | ### Working with master 38 | 39 | Install EDA pegged to master: 40 | 41 | ``` 42 | npm i --save-dev ember-django-adapter@dustinfarris/ember-django-adapter 43 | ``` 44 | 45 | ### Working with your fork 46 | 47 | Clone and install EDA: 48 | 49 | ``` 50 | git clone git@github.com:yourname/ember-django-adapter 51 | cd ember-django-adapter 52 | npm i && bower i 53 | npm link 54 | ``` 55 | 56 | Install test dependencies in your project, and link your local copy of EDA: 57 | 58 | ``` 59 | cd myproject 60 | bower i pretender 61 | bower i sinonjs 62 | npm link ember-django-adapter 63 | ``` 64 | 65 | 66 | Goals 67 | ----- 68 | 69 | * Support applications built with Django REST Framework and Ember.js by 70 | offering easy-to-use addons, and providing documentation and guidance. 71 | * Ensure as much as possible that the Ember.js and Django REST Framework 72 | documentation is up-to-date and accurate as it pertains to their combined 73 | usage. 74 | * Promote the adoption of Ember.js and Django REST Framework and actively take 75 | part in their respective communities. 76 | 77 | 78 | 79 | [Ember Data]: https://github.com/emberjs/data 80 | [Django REST Framework]: http://www.django-rest-framework.org/ 81 | [ember-cli]: http://www.ember-cli.com/ 82 | [ember-django-adapter/issues]: https://github.com/dustinfarris/ember-django-adapter/issues 83 | [dustinfarris.github.io/ember-django-adapter]: https://dustinfarris.github.io/ember-django-adapter/ 84 | [coalesce-find-requests-option]: http://emberjs.com/api/data/classes/DS.RESTAdapter.html#property_coalesceFindRequests 85 | -------------------------------------------------------------------------------- /addon/adapters/drf.js: -------------------------------------------------------------------------------- 1 | import { isArray } from '@ember/array'; 2 | import { dasherize } from '@ember/string'; 3 | import RESTAdapter from 'ember-data/adapters/rest'; 4 | import { pluralize } from 'ember-inflector'; 5 | import DS from "ember-data"; 6 | 7 | const { AdapterError, InvalidError } = DS; 8 | 9 | const ERROR_MESSAGES = { 10 | 401: 'Unauthorized', 11 | 500: 'Internal Server Error' 12 | }; 13 | 14 | /** 15 | * The Django REST Framework adapter allows your store to communicate 16 | * with Django REST Framework-built APIs by adjusting the JSON and URL 17 | * structure implemented by Ember Data to match that of DRF. 18 | * 19 | * The source code for the RESTAdapter superclass can be found at: 20 | * https://github.com/emberjs/data/blob/master/packages/ember-data/lib/adapters/rest_adapter.js 21 | * 22 | * @class DRFAdapter 23 | * @constructor 24 | * @extends DS.RESTAdapter 25 | */ 26 | export default RESTAdapter.extend({ 27 | defaultSerializer: "DS/djangoREST", 28 | addTrailingSlashes: true, 29 | nonFieldErrorsKey: 'non_field_errors', 30 | 31 | 32 | /** 33 | * Determine the pathname for a given type. 34 | * 35 | * @method pathForType 36 | * @param {String} type 37 | * @return {String} path 38 | */ 39 | pathForType: function(type) { 40 | var dasherized = dasherize(type); 41 | return pluralize(dasherized); 42 | }, 43 | 44 | /** 45 | Builds a URL for a given model name and optional ID. 46 | 47 | By default, it pluralizes the type's name (for example, 'post' 48 | becomes 'posts' and 'person' becomes 'people'). 49 | 50 | If an ID is specified, it adds the ID to the path generated 51 | for the type, separated by a `/`. 52 | 53 | If the adapter has the property `addTrailingSlashes` set to 54 | true, a trailing slash will be appended to the result. 55 | 56 | @method buildURL 57 | @param {String} modelName 58 | @param {(String|Array|Object)} id single id or array of ids or query 59 | @param {(DS.Snapshot|Array)} snapshot single snapshot or array of snapshots 60 | @param {String} requestType 61 | @param {Object} query object of query parameters to send for query requests. 62 | @return {String} url 63 | */ 64 | buildURL: function(modelName, id, snapshot, requestType, query) { 65 | var url = this._super(modelName, id, snapshot, requestType, query); 66 | if (this.get('addTrailingSlashes')) { 67 | if (url.charAt(url.length - 1) !== '/') { 68 | url += '/'; 69 | } 70 | } 71 | return url; 72 | }, 73 | 74 | /** 75 | Takes an ajax response, and returns the json payload or an error. 76 | 77 | By default this hook just returns the json payload passed to it. 78 | You might want to override it in two cases: 79 | 80 | 1. Your API might return useful results in the response headers. 81 | Response headers are passed in as the second argument. 82 | 83 | 2. Your API might return errors as successful responses with status code 84 | 200 and an Errors text or object. You can return a `DS.InvalidError` or a 85 | `DS.AdapterError` (or a sub class) from this hook and it will automatically 86 | reject the promise and put your record into the invalid or error state. 87 | 88 | Returning a `DS.InvalidError` from this method will cause the 89 | record to transition into the `invalid` state and make the 90 | `errors` object available on the record. When returning an 91 | `DS.InvalidError` the store will attempt to normalize the error data 92 | returned from the server using the serializer's `extractErrors` 93 | method. 94 | 95 | @method handleResponse 96 | @param {Number} status 97 | @param {Object} headers 98 | @param {Object} payload 99 | @return {Object | DS.AdapterError} response 100 | */ 101 | handleResponse: function(status, headers, payload) { 102 | if (this.isSuccess(status, headers, payload)) { 103 | return payload; 104 | } else if (this.isInvalid(status, headers, payload)) { 105 | return new InvalidError(this._drfToJsonAPIValidationErrors(payload)); 106 | } 107 | 108 | if (Object.getOwnPropertyNames(payload).length === 0) { 109 | payload = ''; 110 | } else if (payload.detail) { 111 | payload = payload.detail; 112 | } 113 | let errors = this.normalizeErrorResponse(status, headers, payload); 114 | 115 | if (ERROR_MESSAGES[status]) { 116 | return new AdapterError(errors, ERROR_MESSAGES[status]); 117 | } 118 | return new AdapterError(errors); 119 | }, 120 | 121 | isInvalid: function(status) { 122 | return status === 400; 123 | }, 124 | 125 | /** 126 | Convert validation errors to a JSON API object. 127 | 128 | Non-field errors are converted to an object that points at /data. Field- 129 | specific errors are converted to an object that points at the respective 130 | attribute. Nested field-specific errors are converted to an object that 131 | include a slash-delimited pointer to the nested attribute. 132 | 133 | NOTE: Because JSON API does not technically support nested resource objects 134 | at this time, any nested errors are literally "in name" only. The 135 | error object will be attached to the parent resource and the nested 136 | object's isValid property will continue to be true. 137 | 138 | @method _drfToJsonAPIValidationErrors 139 | @param {Object} payload 140 | @param {String} keyPrefix Used to recursively process nested errors 141 | @return {Array} A list of JSON API compliant error objects 142 | */ 143 | _drfToJsonAPIValidationErrors(payload, keyPrefix='') { 144 | let out = []; 145 | 146 | payload = this._formatPayload(payload); 147 | 148 | for (let key in payload) { 149 | /*jshint loopfunc: true */ 150 | if (payload.hasOwnProperty(key)) { 151 | if (isArray(payload[key])) { 152 | payload[key].forEach(error => { 153 | if (key === this.get('nonFieldErrorsKey')) { 154 | out.push({ 155 | source: { pointer: '/data' }, 156 | detail: error, 157 | title: 'Validation Error' 158 | }); 159 | } else { 160 | out.push({ 161 | source: { pointer: `/data/attributes/${keyPrefix}${key}` }, 162 | detail: error, 163 | title: 'Invalid Attribute' 164 | }); 165 | } 166 | }); 167 | } else { 168 | out = out.concat( 169 | this._drfToJsonAPIValidationErrors(payload[key], `${keyPrefix}${key}/`) 170 | ); 171 | } 172 | } 173 | } 174 | return out; 175 | }, 176 | 177 | /** 178 | * This is used by RESTAdapter._drfToJsonAPIValidationErrors. 179 | * 180 | * Map string values to arrays because improperly formatted payloads cause 181 | * a maximum call stack size exceeded error 182 | * 183 | * @method _formatPayload 184 | * @param {Object} payload 185 | * @return {Object} payload 186 | */ 187 | _formatPayload: function(payload) { 188 | for (let key in payload) { 189 | if (payload.hasOwnProperty(key)) { 190 | if (typeof payload[key] === 'string') { 191 | payload[key] = [payload[key]]; 192 | } 193 | } 194 | } 195 | 196 | return payload; 197 | }, 198 | 199 | /** 200 | * This is used by RESTAdapter.groupRecordsForFindMany. 201 | * 202 | * The original implementation does not handle trailing slashes well. 203 | * Additionally, it is a complex stripping of the id from the URL, 204 | * which can be dramatically simplified by just returning the base 205 | * URL for the type. 206 | * 207 | * @method _stripIDFromURL 208 | * @param {DS.Store} store 209 | * @param {DS.Snapshot} snapshot 210 | * @return {String} url 211 | */ 212 | _stripIDFromURL: function(store, snapshot) { 213 | return this.buildURL(snapshot.modelName); 214 | } 215 | }); 216 | -------------------------------------------------------------------------------- /addon/serializers/drf.js: -------------------------------------------------------------------------------- 1 | import { decamelize } from '@ember/string'; 2 | import { isNone } from '@ember/utils'; 3 | import RESTSerializer from 'ember-data/serializers/rest'; 4 | 5 | /** 6 | * Handle JSON/REST (de)serialization. 7 | * 8 | * This serializer adjusts payload data so that it is consumable by 9 | * Django REST Framework API endpoints. 10 | * 11 | * @class DRFSerializer 12 | * @extends DS.RESTSerializer 13 | */ 14 | export default RESTSerializer.extend({ 15 | // Remove this in our 2.0 release. 16 | isNewSerializerAPI: true, 17 | 18 | /** 19 | * Returns the resource's relationships formatted as a JSON-API "relationships object". 20 | * 21 | * http://jsonapi.org/format/#document-resource-object-relationships 22 | * 23 | * This version adds a 'links'hash with relationship urls before invoking the 24 | * JSONSerializer's version. 25 | * 26 | * @method extractRelationships 27 | * @param {Object} modelClass 28 | * @param {Object} resourceHash 29 | * @return {Object} 30 | */ 31 | extractRelationships: function (modelClass, resourceHash) { 32 | if (!resourceHash.hasOwnProperty('links')) { 33 | resourceHash['links'] = {}; 34 | } 35 | 36 | modelClass.eachRelationship(function(key, relationshipMeta) { 37 | let payloadRelKey = this.keyForRelationship(key); 38 | 39 | if (!resourceHash.hasOwnProperty(payloadRelKey)) { 40 | return; 41 | } 42 | 43 | if (relationshipMeta.kind === 'hasMany' || relationshipMeta.kind === 'belongsTo') { 44 | // Matches strings starting with: https://, http://, //, / 45 | var payloadRel = resourceHash[payloadRelKey]; 46 | if (!isNone(payloadRel) && !isNone(payloadRel.match) && 47 | typeof(payloadRel.match) === 'function' && payloadRel.match(/^((https?:)?\/\/|\/)\w/)) { 48 | resourceHash['links'][key] = resourceHash[payloadRelKey]; 49 | delete resourceHash[payloadRelKey]; 50 | } 51 | } 52 | }, this); 53 | 54 | return this._super(modelClass, resourceHash); 55 | }, 56 | 57 | /** 58 | * Returns the number extracted from the page number query param of 59 | * a `url`. `null` is returned when the page number query param 60 | * isn't present in the url. `null` is also returned when `url` is 61 | * `null`. 62 | * 63 | * @method extractPageNumber 64 | * @private 65 | * @param {String} url 66 | * @return {Number} page number 67 | */ 68 | extractPageNumber: function(url) { 69 | var match = /.*?[?&]page=(\d+).*?/.exec(url); 70 | if (match) { 71 | return Number(match[1]).valueOf(); 72 | } 73 | return null; 74 | }, 75 | 76 | /** 77 | * Converts DRF API server responses into the format expected by the RESTSerializer. 78 | * 79 | * If the payload has DRF metadata and results properties, all properties that aren't in 80 | * the results are added to the 'meta' hash so that Ember Data can use these properties 81 | * for metadata. The next and previous pagination URLs are parsed to make it easier to 82 | * paginate data in applications. The RESTSerializer's version of this function is called 83 | * with the converted payload. 84 | * 85 | * @method normalizeResponse 86 | * @param {DS.Store} store 87 | * @param {DS.Model} primaryModelClass 88 | * @param {Object} payload 89 | * @param {String|Number} id 90 | * @param {String} requestType 91 | * @return {Object} JSON-API Document 92 | */ 93 | normalizeResponse: function (store, primaryModelClass, payload, id, requestType) { 94 | let convertedPayload = {}; 95 | 96 | if (!isNone(payload) && 97 | payload.hasOwnProperty('next') && 98 | payload.hasOwnProperty('previous') && 99 | payload.hasOwnProperty('results')) { 100 | 101 | // Move DRF metadata to the meta hash. 102 | convertedPayload[primaryModelClass.modelName] = JSON.parse(JSON.stringify(payload.results)); 103 | delete payload.results; 104 | convertedPayload['meta'] = JSON.parse(JSON.stringify(payload)); 105 | 106 | // The next and previous pagination URLs are parsed to make it easier to paginate data in applications. 107 | if (!isNone(convertedPayload.meta['next'])) { 108 | convertedPayload.meta['next'] = this.extractPageNumber(convertedPayload.meta['next']); 109 | } 110 | if (!isNone(convertedPayload.meta['previous'])) { 111 | let pageNumber = this.extractPageNumber(convertedPayload.meta['previous']); 112 | // The DRF previous URL doesn't always include the page=1 query param in the results for page 2. We need to 113 | // explicitly set previous to 1 when the previous URL is defined but the page is not set. 114 | if (isNone(pageNumber)) { 115 | pageNumber = 1; 116 | } 117 | convertedPayload.meta['previous'] = pageNumber; 118 | } 119 | } else { 120 | convertedPayload[primaryModelClass.modelName] = JSON.parse(JSON.stringify(payload)); 121 | } 122 | 123 | // return single result for requestType 'queryRecord' 124 | let records = convertedPayload[primaryModelClass.modelName]; 125 | if (requestType === 'queryRecord' && Array.isArray(records)) { 126 | let first = records.length > 0 ? records[0] : null; 127 | convertedPayload[primaryModelClass.modelName] = first; 128 | } 129 | 130 | return this._super(store, primaryModelClass, convertedPayload, id, requestType); 131 | }, 132 | 133 | /** 134 | * You can use this method to customize how a serialized record is 135 | * added to the complete JSON hash to be sent to the server. By 136 | * default the JSON Serializer does not namespace the payload and 137 | * just sends the raw serialized JSON object. 138 | * 139 | * If your server expects namespaced keys, you should consider using 140 | * the RESTSerializer. Otherwise you can override this method to 141 | * customize how the record is added to the hash. 142 | * 143 | * For example, your server may expect underscored root objects. 144 | * 145 | * @method serializeIntoHash 146 | * @param {Object} hash 147 | * @param {subclass of DS.Model} type 148 | * @param {DS.Snapshot} snapshot 149 | * @param {Object} options 150 | */ 151 | serializeIntoHash: function(hash, type, snapshot, options) { 152 | Object.assign(hash, this.serialize(snapshot, options)); 153 | }, 154 | 155 | /** 156 | * `keyForAttribute` can be used to define rules for how to convert 157 | * an attribute name in your model to a key in your JSON. 158 | * 159 | * @method keyForAttribute 160 | * @param {String} key 161 | * @return {String} normalized key 162 | */ 163 | keyForAttribute: function(key) { 164 | return decamelize(key); 165 | }, 166 | 167 | /** 168 | * `keyForRelationship` can be used to define a custom key when 169 | * serializing relationship properties. By default `JSONSerializer` 170 | * does not provide an implementation of this method. 171 | * 172 | * @method keyForRelationship 173 | * @param {String} key 174 | * @return {String} normalized key 175 | */ 176 | keyForRelationship: function(key) { 177 | return decamelize(key); 178 | } 179 | }); 180 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/app/.gitkeep -------------------------------------------------------------------------------- /app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import DRFAdapter from './drf'; 2 | 3 | export default DRFAdapter; 4 | -------------------------------------------------------------------------------- /app/adapters/drf.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import DRFAdapter from 'ember-django-adapter/adapters/drf'; 3 | import ENV from '../config/environment'; 4 | 5 | export default DRFAdapter.extend({ 6 | host: computed(function() { 7 | return ENV.APP.API_HOST; 8 | }), 9 | 10 | namespace: computed(function() { 11 | return ENV.APP.API_NAMESPACE; 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import DRFSerializer from './drf'; 2 | 3 | export default DRFSerializer; 4 | -------------------------------------------------------------------------------- /app/serializers/drf.js: -------------------------------------------------------------------------------- 1 | import DRFSerializer from 'ember-django-adapter/serializers/drf'; 2 | 3 | export default DRFSerializer; 4 | -------------------------------------------------------------------------------- /blueprints/drf-adapter/files/app/adapters/__name__.js: -------------------------------------------------------------------------------- 1 | import DRFAdapter from './drf'; 2 | 3 | export default DRFAdapter.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /blueprints/drf-adapter/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Generates a drf-adapter for you to customize.' 3 | 4 | // locals: function(options) { 5 | // // Return custom template variables here. 6 | // return { 7 | // foo: options.entity.options.foo 8 | // }; 9 | // } 10 | 11 | // afterInstall: function(options) { 12 | // // Perform extra work here. 13 | // } 14 | }; 15 | -------------------------------------------------------------------------------- /blueprints/drf-serializer/files/app/serializers/__name__.js: -------------------------------------------------------------------------------- 1 | import DRFSerializer from './drf'; 2 | 3 | export default DRFSerializer.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /blueprints/drf-serializer/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description: 'Generates a drf-serializer for you to customize.' 3 | 4 | // locals: function(options) { 5 | // // Return custom template variables here. 6 | // return { 7 | // foo: options.entity.options.foo 8 | // }; 9 | // } 10 | 11 | // afterInstall: function(options) { 12 | // // Perform extra work here. 13 | // } 14 | }; 15 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = function() { 6 | return Promise.all([ 7 | getChannelURL('release'), 8 | getChannelURL('beta'), 9 | getChannelURL('canary') 10 | ]).then((urls) => { 11 | return { 12 | useYarn: true, 13 | scenarios: [ 14 | { 15 | name: 'ember-lts-2.12', 16 | npm: { 17 | dependencies: { 18 | 'ember-data': '^2.12.0' 19 | }, 20 | devDependencies: { 21 | 'ember-source': '^2.12.0' 22 | } 23 | } 24 | }, 25 | { 26 | name: 'ember-lts-2.16', 27 | npm: { 28 | dependencies: { 29 | 'ember-data': '^2.16.0' 30 | }, 31 | devDependencies: { 32 | 'ember-source': '^2.16.0' 33 | } 34 | } 35 | }, 36 | { 37 | name: 'ember-release', 38 | npm: { 39 | devDependencies: { 40 | 'ember-data': 'latest', 41 | 'ember-source': urls[0] 42 | } 43 | } 44 | }, 45 | { 46 | name: 'ember-beta', 47 | npm: { 48 | devDependencies: { 49 | 'ember-data': 'beta', 50 | 'ember-source': urls[1] 51 | } 52 | }, 53 | allowedToFail: true 54 | }, 55 | { 56 | name: 'ember-canary', 57 | npm: { 58 | devDependencies: { 59 | 'ember-data': 'canary', 60 | 'ember-source': urls[2] 61 | } 62 | }, 63 | allowedToFail: true 64 | } 65 | ] 66 | } 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | var ENV = { 5 | APP: { 6 | API_HOST: 'http://localhost:8000', 7 | API_NAMESPACE: 'api', 8 | API_ADD_TRAILING_SLASHES: true 9 | } 10 | }; 11 | return ENV; 12 | }; 13 | -------------------------------------------------------------------------------- /docs/coalesce-find-requests.md: -------------------------------------------------------------------------------- 1 | # Coalescing Find Requests 2 | 3 | When a record returns the IDs of records in a hasMany relationship, Ember Data 4 | allows us to opt-in to combine these requests into a single request. 5 | 6 | **Note:** Using [hyperlinked related fields](hyperlinked-related-fields.md) to retrieve related 7 | records in a single request is preferred over using coalesceFindRequests since there is a limit on 8 | the number of records per request on read-only fields due to URL length restrictions. 9 | 10 | Suppose you have Ember models: 11 | 12 | ```js 13 | // app/models/person.js 14 | 15 | import DS from 'ember-data'; 16 | 17 | export default DS.Model.extend({ 18 | name: DS.attr('string'), 19 | pets: DS.hasMany('pet', { async: True }) 20 | }); 21 | 22 | 23 | // app/models/pet.js 24 | 25 | import DS from 'ember-data'; 26 | 27 | export default DS.Model.extend({ 28 | age: DS.attr('number') 29 | }); 30 | ``` 31 | 32 | An out-of-the-box DRF model serializer for Person would return something like 33 | this: 34 | 35 | ``` 36 | GET /api/people/1/ 37 | 38 | { 39 | "id": 1, 40 | "name": "Fred", 41 | "pets": [1, 2, 3] 42 | } 43 | ``` 44 | 45 | When Ember Data decides to resolve the pets, by default it would fire 3 46 | separate requests. In this case: 47 | 48 | ``` 49 | GET /api/pets/1/ 50 | 51 | { 52 | "id": 1, 53 | "age": 5 54 | } 55 | 56 | GET /api/pets/2/ 57 | 58 | { 59 | "id": 2, 60 | "age": 5 61 | } 62 | 63 | GET /api/pets/3/ 64 | 65 | { 66 | "id": 3, 67 | "age": 6 68 | } 69 | ``` 70 | 71 | However, if we opt-in to coalesceFindRequests, we can consolidate this into 72 | 1 call. 73 | 74 | 75 | ## Enable coalesceFindRequests 76 | 77 | [Extend](extending.md) the adapter, and enable coalesceFindRequests: 78 | 79 | ```js 80 | // app/adapters/application.js 81 | 82 | import DRFAdapter from './drf'; 83 | 84 | export default DRFAdapter.extend({ 85 | coalesceFindRequests: true 86 | }); 87 | ``` 88 | 89 | Now, when Ember Data resolves the pets, it will fire a request that looks like 90 | this: 91 | 92 | ``` 93 | GET /api/pets/?ids[]=1&ids[]=2&ids[]=3 94 | ``` 95 | 96 | 97 | ## CoalesceFilterBackend 98 | 99 | All this is great, except Django REST Framework is not quite able to handle such a 100 | request out of the box. Thankfully, DRF 101 | [allows you to plug in custom filters](http://www.django-rest-framework.org/api-guide/filtering/#setting-filter-backends), 102 | and writing a filter for this kind of request is super simple. 103 | 104 | In your project somewhere, write the following filter: 105 | 106 | ```python 107 | # myapp/filters.py 108 | 109 | from rest_framework import filters 110 | 111 | 112 | class CoalesceFilterBackend(filters.BaseFilterBackend): 113 | """ 114 | Support Ember Data coalesceFindRequests. 115 | 116 | """ 117 | def filter_queryset(self, request, queryset, view): 118 | id_list = request.query_params.getlist('ids[]') 119 | if id_list: 120 | # Disable pagination, so all records can load. 121 | view.pagination_class = None 122 | queryset = queryset.filter(id__in=id_list) 123 | return queryset 124 | ``` 125 | 126 | Now you just need to add this filter to `filter_backends` in your views, e.g.: 127 | 128 | ```python 129 | from myapp.filters import CoalesceFilterBackend 130 | 131 | 132 | class UserListView(generics.ListAPIView): 133 | queryset = User.objects.all() 134 | serializer = UserSerializer 135 | filter_backends = (CoalesceFilterBackend,) 136 | ``` 137 | 138 | Or, configure it globally in your DRF settings: 139 | 140 | ```python 141 | REST_FRAMEWORK = { 142 | 'DEFAULT_FILTER_BACKENDS': ('myapp.filters.CoalesceFilterBackend',) 143 | } 144 | ``` 145 | 146 | Now, when Ember Data sends the coalesced request, DRF will return meaningful 147 | data: 148 | 149 | ``` 150 | GET /api/pets/?ids[]=1&ids[]=2&ids[]=3 151 | 152 | [ 153 | { 154 | "id": 1, 155 | "age": 5 156 | }, 157 | { 158 | "id": 2, 159 | "age": 5 160 | }, 161 | { 162 | "id": 3, 163 | "age": 6 164 | } 165 | ] 166 | ``` 167 | -------------------------------------------------------------------------------- /docs/configuring.md: -------------------------------------------------------------------------------- 1 | # Configuring 2 | 3 | There are a number of configuration variables you can set in your environment.js file. 4 | 5 | 6 | ## API_HOST 7 | 8 | **Default:** `'http://localhost:8000'` 9 | 10 | The fully-qualified host of your API server. 11 | 12 | 13 | ## API_NAMESPACE 14 | 15 | **Default:** `'api'` 16 | 17 | The URL prefix (namespace) for your API. In other words, if you set this to my-api/v1, then all 18 | API requests will look like /my-api/v1/users/56/, or similar. 19 | 20 | 21 | ## Example 22 | 23 | ```js 24 | // my-ember-cli-project/config/environment.js 25 | 26 | module.exports = function(environment) { 27 | var ENV = { 28 | APP: { 29 | } 30 | }; 31 | 32 | if (environment === 'development') { 33 | ENV.APP.API_HOST = 'http://localhost:8000'; 34 | } 35 | 36 | if (environment === 'production') { 37 | ENV.APP.API_HOST = 'https://api.myproject.com'; 38 | ENV.APP.API_NAMESPACE = 'v2'; 39 | } 40 | 41 | return ENV; 42 | }; 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ember Django Adapter 2 | 3 | We welcome all bug reports and pull requests. 4 | 5 | Issues: [github.com/dustinfarris/ember-django-adapter/issues][1] 6 | 7 | 8 | ## Local Development 9 | 10 | To run ember-django-adapter locally, you will have to use `npm link`. 11 | 12 | ``` 13 | git clone git@github.com:dustinfarris/ember-django-adapter 14 | cd ember-django-adapter 15 | npm i && bower i 16 | npm link 17 | ``` 18 | 19 | Then in a test ember-cli project, you will have to install a couple bower dependencies separately. 20 | These are not actually required in the test ember-cli project, but errors will be thrown if they 21 | are not present. 22 | 23 | ``` 24 | cd test-ember-cli-project 25 | bower i pretender 26 | bower i http://sinonjs.org/releases/sinon-1.12.1.js 27 | npm link ember-django-adapter 28 | ``` 29 | 30 | Now your test project is using a symlinked copy of your local ember-django-adapter, and any changes 31 | you make to the adapter will be reflected in your project in real-time. 32 | 33 | 34 | ## Running tests 35 | 36 | You can run tests with the latest supported Ember Data beta with: 37 | 38 | ``` 39 | ember test 40 | ``` 41 | 42 | You can also run tests against Ember Data canary with: 43 | 44 | ``` 45 | ember try ember-data-canary 46 | ``` 47 | 48 | 49 | [1]: https://github.com/dustinfarris/ember-django-adapter/issues 50 | -------------------------------------------------------------------------------- /docs/embedded-records.md: -------------------------------------------------------------------------------- 1 | # Embedded Records 2 | 3 | Let's say you've set up a serializer in DRF that has embedded records. For example: 4 | 5 | ```python 6 | class Person(models.Model): 7 | name = models.CharField(max_length=20) 8 | 9 | 10 | class Pet(models.Model): 11 | name = models.CharField(max_length=20) 12 | owner = models.ForeignKey(Person, related_name='pets') 13 | 14 | 15 | class PetSerializer(serializers.ModelSerializer): 16 | class Meta: 17 | model = Pet 18 | 19 | 20 | class PersonSerializer(serializers.ModelSerializer): 21 | pets = PetSerializer(many=True) 22 | 23 | class Meta: 24 | model = Person 25 | ``` 26 | 27 | On the Ember side, your models would look like this: 28 | 29 | ```js 30 | // app/models/pet.js 31 | 32 | import DS from 'ember-data'; 33 | 34 | export default DS.Model.extend({ 35 | person: DS.belongsTo('person'), 36 | name: DS.attr('string') 37 | }); 38 | ``` 39 | 40 | ```js 41 | // app/models/person.js 42 | 43 | import DS from 'ember-data'; 44 | 45 | export default DS.Model.extend({ 46 | name: DS.attr('string'), 47 | pets: DS.hasMany('pet') 48 | }); 49 | ``` 50 | 51 | The API's JSON response from such a setup would look something like this: 52 | 53 | ```json 54 | { 55 | "id": 2, 56 | "name": "Frank", 57 | "pets": [ 58 | { "id": 1, "name": "Spot" }, 59 | { "id": 2, "name": "Fido" } 60 | ] 61 | } 62 | ``` 63 | 64 | Ember Data supports this sort of response (since 1.0.0-beta.10), but you will have to extend the 65 | serializer for this model to make Ember Data aware of it. 66 | 67 | In your Ember project, create a DRF serializer for your Person model. 68 | 69 | ```console 70 | ember generate drf-serializer person 71 | ``` 72 | 73 | This creates a skeleton serializer that extends the DRF serializer in app/serializers/person.js. 74 | Modify this file to support the embedded records: 75 | 76 | ```js 77 | // app/serializers/person.js 78 | 79 | import DRFSerializer from './drf'; 80 | import DS from 'ember-data'; 81 | 82 | export default DRFSerializer.extend(DS.EmbeddedRecordsMixin, { 83 | attrs: { 84 | pets: { embedded: 'always' } 85 | } 86 | }); 87 | ``` 88 | 89 | 90 | ### Writable nested records 91 | 92 | Writable nested records are not supported by the adapter at this time. 93 | -------------------------------------------------------------------------------- /docs/extending.md: -------------------------------------------------------------------------------- 1 | # Extending the adapter/serializer 2 | 3 | More than likely, you will need to add you own tweaks to the adapter and (more often) the 4 | serializer. EDA provides blueprints to make this easy. For example, to make a customizable 5 | serializer for a User model: 6 | 7 | ```console 8 | ember generate drf-serializer user 9 | ``` 10 | 11 | This will create app/serializers/user.js for you to customize. 12 | 13 | Similarly, if you wanted to, for example, extend the adapter on the application level: 14 | 15 | ```console 16 | ember generate drf-adapter application 17 | ``` 18 | 19 | This will create app/adapters/application.js for you to customize. 20 | -------------------------------------------------------------------------------- /docs/hyperlinked-related-fields.md: -------------------------------------------------------------------------------- 1 | # Hyperlinked Related Fields 2 | 3 | Ember Django Adapter has support for Django REST Framework's 4 | [HyperlinkedRelatedField](http://www.django-rest-framework.org/api-guide/relations/#hyperlinkedrelatedfield). 5 | URLs in a json hash for `hasMany` and `belongsTo` relationship fields will automatically be 6 | retrieved and added to the store. 7 | 8 | This feature has limited use without some configuration because related records 9 | are returned as multiple URLs which produces multiple requests. Sending multiple 10 | requests becomes a performance bottleneck when there are more than a few related 11 | URLs in the json hash. 12 | 13 | For example, this blog post json hash shows how a `hasMany` relationship is 14 | serialized by the default configuration of `HyperlinkedRelatedField(many=True)`: 15 | 16 | 17 | ```json 18 | { 19 | "id": 11, 20 | "title": "title 11", 21 | "body": "post 11", 22 | "comments": [ 23 | "http://example.com/api/comments/9/", 24 | "http://example.com/api/comments/10/", 25 | "http://example.com/api/comments/11/" 26 | ] 27 | } 28 | ``` 29 | 30 | As alluded to previously, related records can be retrieved in a single request 31 | by creating a custom `ViewSet` that allows the related records to be retrieved 32 | from a nested URL. 33 | 34 | For example, the blog post json hash would now have a single URL for the 35 | related comments instead of one URL per related record: 36 | 37 | ```json 38 | { 39 | "id": 11, 40 | "title": "title 11", 41 | "body": "post 11", 42 | "comments": "http://example.com/api/posts/11/comments/" 43 | } 44 | ``` 45 | 46 | **Note:** It is also possible to use the [Coalesce Find Requests](coalesce-find-requests.md) 47 | feature to retrieve related records in a single request, however, this is the preferred 48 | solution. 49 | 50 | ## Models, Serializers and Router 51 | 52 | We can create a blog post hash with the related comments URL by using the 53 | following models, serializers and router: 54 | 55 | ```python 56 | # models.py 57 | 58 | class Post(models.Model): 59 | title = models.CharField(max_length=100) 60 | body = models.TextField() 61 | 62 | 63 | class Comment(models.Model): 64 | body = models.TextField() 65 | post = models.ForeignKey('Post', related_name='comments') 66 | 67 | 68 | # serializers.py 69 | 70 | class PostSerializer(serializers.HyperlinkedModelSerializer): 71 | comments = serializers.HyperlinkedIdentityField(view_name='post-comments') 72 | 73 | class Meta: 74 | model = Post 75 | fields = ('id', 'title', 'body', 'comments') 76 | 77 | 78 | class CommentSerializer(serializers.HyperlinkedModelSerializer): 79 | 80 | class Meta: 81 | model = Comment 82 | fields = ('id', 'body', 'post') 83 | 84 | # urls.py 85 | 86 | router = DefaultRouter() 87 | router.register(r'posts', PostViewSet) 88 | urlpatterns = router.urls 89 | ``` 90 | 91 | ## ViewSet 92 | 93 | The nested comments URL is achieved by adding a [@detail_route](www.django-rest-framework.org/api-guide/routers/) 94 | decorator on the `comments` method of the `PostViewSet`. The related comments 95 | are manually retrieved from the database and serialized. 96 | 97 | ```python 98 | # views.py 99 | 100 | class PostViewSet(viewsets.ModelViewSet): 101 | serializer_class = PostSerializer 102 | queryset = Post.objects.all() 103 | 104 | @detail_route() 105 | def comments(self, request, pk=None): 106 | post = self.get_object() 107 | serializer = CommentSerializer(post.comments.all(), context={'request': request}, many=True) 108 | return Response(serializer.data) 109 | ``` 110 | 111 | For example, retrieving the comments `@detail_route` using this nested URL 112 | `http://example.com/api/posts/11/comments/` returns all of the related comments 113 | for post 11. 114 | 115 | ```json 116 | [ 117 | { 118 | "id": 9, 119 | "body": "comment 9", 120 | "post": "http://example.com/api/posts/11/" 121 | }, 122 | { 123 | "id": 14, 124 | "body": "comment 14", 125 | "post": "http://example.com/api/posts/11/" 126 | } 127 | ] 128 | ``` 129 | 130 | ## Write Operations 131 | 132 | In this example, the comments `@detail_route` is read-only. If you need to perform 133 | write operations on the specific related records (e.g. create, update or delete 134 | specific comments), you would need to add a top level API with the required operations 135 | for the related model (e.g.`CommentViewSet` on the `/api/comments/` resource). 136 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Ember Django Adapter Documentation 2 | 3 | Ember Django Adapter (EDA) enables users to build applications using Django REST Framework and 4 | Ember.js. The two packages work with REST APIs, but differ on certain JSON formatting and 5 | semantics. Fortunately, Ember Data (the library powering Ember.js models) provides an opportunity 6 | to write custom adapters to bridge these differences. EDA is one such adapter specifically 7 | designed to work with Django REST Framework. 8 | 9 | 10 | ## Requirements 11 | 12 | To build a project using Ember Django Adapter, you will need to be using: 13 | 14 | * Django REST Framework >= 3.0 15 | * Ember Data >= 1.13.7 **note:** Ember Data 1.13.x requires ember 1.12.1 and later 16 | * Ember CLI >= 0.2.7 17 | 18 | 19 | ## Quickstart 20 | 21 | In your Ember CLI project, install Ember Django Adapter from the command line: 22 | 23 | ```bash 24 | ember install ember-django-adapter 25 | ``` 26 | 27 | See [configuring](configuring.md) for more information on customizing the adapter. 28 | -------------------------------------------------------------------------------- /docs/non-field-errors.md: -------------------------------------------------------------------------------- 1 | # Non field errors 2 | 3 | 4 | By default, Django REST Framework stores non field errors in a key named 'non_field_errors' 5 | whenever there is a validation error (HTTP status 400). 6 | Django REST Framework allows to customize the name of this key. 7 | 8 | If you changed the default value, you will need to [extend](extending.md) the adapter and set 9 | `nonFieldErrorsKey` in `app/adapters/application.js`: 10 | 11 | ```js 12 | // app/adapters/application.js 13 | 14 | import DRFAdapter from './drf'; 15 | 16 | export default DRFAdapter.extend({ 17 | nonFieldErrorKey: 'my_key_name_is_waaay_cooler' 18 | }); 19 | ``` 20 | 21 | In the case of an InvalidError being raised by the adapter when the response contains non-field errors, the 22 | adapter will include in the errors array a jsonapi error object of the form: 23 | 24 | ```js 25 | { detail: 'error 1', source: { pointer: 'data' }, title: 'Validation Error' } //or whatever key name you configured 26 | ``` 27 | 28 | In case of several errors, the InvalidError.errors attribute will include 29 | 30 | ```js 31 | { detail: 'error 1', source: { pointer: 'data' }, title: 'Validation Error' } //or whatever key name you configured 32 | { detail: 'error 2', source: { pointer: 'data' }, title: 'Validation Error' } //or whatever key name you configured 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | Pagination is supported using the metadata support that is built into Ember Data. 4 | Metadata from Django REST Framework paginated list views is updated on every request 5 | to the server. 6 | 7 | The pagination support in EDA works with the default pagination setup in DRF 3.0 and the 8 | [PageNumberPagination](http://www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination) 9 | class in DRF 3.1. It's possible to use the other DRF 3.1 pagination classes by 10 | overriding `extractMeta` (see [customizing the Metadata](#customizing-the-metadata) below). 11 | 12 | 13 | ## Accessing the Metadata 14 | 15 | To get a page of records, simply run a `query` request with the `page` query param. 16 | 17 | ```js 18 | let result = this.store.query('post', {page: 1}); 19 | ``` 20 | 21 | All of the DRF metadata (including the pagination metadata) can be access through the 22 | `meta` property of the result once the promise is fulfilled. 23 | 24 | ```js 25 | let meta = result.get('meta'); 26 | ``` 27 | 28 | **Note:** The `meta` property will only be set on results of `query` requests. 29 | 30 | 31 | ## Pagination Metadata 32 | 33 | The pagination metadata consists of three properties that give the application enough 34 | information to paginate through a Django REST Framework paginated list view. 35 | 36 | * `next` - The next page number or `null` when there is no next page (i.e. the last 37 | page). 38 | * `previous` - The previous page number or `null` when there is no previous page (i.e. 39 | the first page). 40 | * `count` - The total number of records available. This can be used along with the page 41 | size to calculate the total number of pages (see 42 | [customizing the Metadata](#customizing-the-metadata) below). 43 | 44 | 45 | The `next` and `previous` page number can be used directly as the value of the `page` 46 | query param. `null` is not a valid value for the `page` query param so applications need 47 | to check if `next` and `previous` are null before using them. 48 | 49 | ```js 50 | if (meta.next) { 51 | result = store.query('post', {page: meta.next}) 52 | } 53 | ``` 54 | 55 | ## Customizing the Metadata 56 | 57 | You can customize the metadata by overriding the `extractMeta` and adding and / or removing 58 | metadata as indicated in this template. 59 | 60 | ```js 61 | // app/serializer/.js 62 | 63 | import Ember from 'ember'; 64 | import DRFSerializer from './drf'; 65 | 66 | export default DRFSerializer.extend({ 67 | extractMeta: function(store, type, payload) { 68 | let meta = this._super(store, type, payload); 69 | if (!Ember.isNone(meta)) { 70 | 71 | // Add or remove metadata here. 72 | 73 | } 74 | return meta; 75 | } 76 | }); 77 | ``` 78 | 79 | This version of `extractMeta` adds the total page count to the `post` metadata. 80 | 81 | ```js 82 | // app/serializer/post.js 83 | 84 | import Ember from 'ember'; 85 | import DRFSerializer from './drf'; 86 | 87 | export default DRFSerializer.extend({ 88 | extractMeta: function(store, type, payload) { 89 | let meta = this._super(store, type, payload); 90 | if (!Ember.isNone(meta)) { 91 | // Add totalPages to metadata. 92 | let totalPages = 1; 93 | if (!Ember.isNone(meta.next)) { 94 | // Any page that is not the last page. 95 | totalPages = Math.ceil(meta.count / payload[type.modelName].length); 96 | } else if (Ember.isNone(meta.next) && !Ember.isNone(meta.previous)) { 97 | // The last page when there is more than one page. 98 | totalPages = meta.previous + 1; 99 | } 100 | meta['totalPages'] = totalPages; 101 | } 102 | return meta; 103 | } 104 | }); 105 | ``` 106 | 107 | ## Cursor Pagination 108 | 109 | To use [`CursorPagination`](http://www.django-rest-framework.org/api-guide/pagination/#cursorpagination), override `extractPageNumber` in the serializer to extract the `cursor`. 110 | 111 | ```js 112 | // app/serializer/drf.js 113 | 114 | import DRFSerializer from 'ember-django-adapter/serializers/drf'; 115 | 116 | export default DRFSerializer.extend({ 117 | extractPageNumber: function(url) { 118 | var match = /.*?[\?&]cursor=([A-Za-z0-9]+).*?/.exec(url); 119 | if (match) { 120 | return match[1]; 121 | } 122 | return null; 123 | } 124 | }); 125 | ``` 126 | 127 | If you don't use the `PageNumberPagination` for pagination with DRF 3.1 you can also add 128 | the metadata for the pagination scheme you use here. We may add support for the other 129 | pagination classes in the future. If this is something you are interested in contributing, 130 | please file an issue on github. 131 | -------------------------------------------------------------------------------- /docs/trailing-slashes.md: -------------------------------------------------------------------------------- 1 | # Trailing Slashes 2 | 3 | 4 | By default, Django REST Framework adds trailing slashes to its generated URLs. 5 | EDA is set up to handle this, however, if you have decided to opt out of 6 | trailing slashes, you will need to extend the adapter with this configuration. 7 | 8 | e.g., if you have set up a router in DRF that is instantiated like this: 9 | 10 | ```python 11 | from rest_framework import routers 12 | 13 | 14 | router = routers.DefaultRouter(trailing_slash=False) 15 | ``` 16 | 17 | then you will need to [extend](extending.md) the adapter and switch off 18 | `addTrailingSlashes` in `app/adapters/application.js`: 19 | 20 | ```js 21 | // app/adapters/application.js 22 | 23 | import DRFAdapter from './drf'; 24 | 25 | export default DRFAdapter.extend({ 26 | addTrailingSlashes: false 27 | }); 28 | ``` 29 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'ember-django-adapter', 5 | 6 | included: function(app) { 7 | this._super.included.apply(this, arguments); 8 | 9 | // see: https://github.com/ember-cli/ember-cli/issues/3718 10 | if (typeof app.import !== 'function' && app.app) { 11 | app = app.app; 12 | } 13 | 14 | app.import('vendor/ember-django-adapter/register-version.js'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Ember Django Adapter 2 | repo_url: https://github.com/dustinfarris/ember-django-adapter 3 | pages: 4 | - Introduction: 'index.md' 5 | - Configuring: 'configuring.md' 6 | - Extending: 'extending.md' 7 | - Trailing Slashes: 'trailing-slashes.md' 8 | - Embedded Records: 'embedded-records.md' 9 | - Pagination: 'pagination.md' 10 | - Coalesce Find Requests: 'coalesce-find-requests.md' 11 | - Hyperlinked Related Fields: 'hyperlinked-related-fields.md' 12 | - Contributing: 'contributing.md' 13 | - Changelog: 'changelog.md' 14 | theme: readthedocs 15 | google_analytics: ['UA-13275015-13', 'auto'] 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-django-adapter", 3 | "version": "2.1.1", 4 | "description": "Use Django REST Framework with your Ember project", 5 | "keywords": [ 6 | "ember-addon", 7 | "adapter", 8 | "django", 9 | "ember-data" 10 | ], 11 | "license": "MIT", 12 | "author": "Dustin Farris ", 13 | "directories": { 14 | "doc": "doc", 15 | "test": "tests" 16 | }, 17 | "repository": "https://github.com/dustinfarris/ember-django-adapter", 18 | "scripts": { 19 | "build": "ember build", 20 | "lint:js": "eslint ./*.js addon addon-test-support app config lib server test-support tests", 21 | "start": "ember serve", 22 | "test": "ember try:each" 23 | }, 24 | "peerDependencies": { 25 | "ember-data": "^3.0.0" 26 | }, 27 | "dependencies": { 28 | "ember-cli-babel": "^6.16.0", 29 | "ember-inflector": "^2.1.0" 30 | }, 31 | "devDependencies": { 32 | "broccoli-asset-rev": "^2.4.5", 33 | "ember-ajax": "^3.0.0", 34 | "ember-cli": "~3.0.4", 35 | "ember-cli-dependency-checker": "^2.0.0", 36 | "ember-cli-eslint": "^4.2.1", 37 | "ember-cli-htmlbars": "^2.0.1", 38 | "ember-cli-htmlbars-inline-precompile": "^1.0.0", 39 | "ember-cli-inject-live-reload": "^1.4.1", 40 | "ember-cli-pretender": "^1.0.1", 41 | "ember-cli-shims": "^1.2.0", 42 | "ember-cli-sri": "^2.1.1", 43 | "ember-cli-uglify": "^2.0.0", 44 | "ember-disable-prototype-extensions": "^1.1.2", 45 | "ember-export-application-global": "^2.0.0", 46 | "ember-load-initializers": "^1.0.0", 47 | "ember-maybe-import-regenerator": "^0.1.6", 48 | "ember-qunit": "^3.4.1", 49 | "ember-resolver": "^4.0.0", 50 | "ember-sinon": "1.0.1", 51 | "ember-source": "^3.0.0", 52 | "ember-source-channel-url": "^1.0.1", 53 | "ember-try": "^1.1.0", 54 | "eslint-plugin-ember": "^5.0.0", 55 | "eslint-plugin-node": "^6.0.1", 56 | "loader.js": "^4.2.3" 57 | }, 58 | "engines": { 59 | "node": "6.* || 8.* || >= 10.*" 60 | }, 61 | "ember-addon": { 62 | "after": "ember-data", 63 | "configPath": "tests/dummy/config" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | mode: 'ci', 13 | args: [ 14 | // --no-sandbox is needed when running Chrome inside a container 15 | process.env.TRAVIS ? '--no-sandbox' : null, 16 | 17 | '--disable-gpu', 18 | '--headless', 19 | '--remote-debugging-port=0', 20 | '--window-size=1440,900' 21 | ].filter(Boolean) 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tests/acceptance/crud-failure-test.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { 3 | module, 4 | test, 5 | } from 'qunit'; 6 | import { setupApplicationTest } from 'ember-qunit'; 7 | import Pretender from 'pretender'; 8 | 9 | var store; 10 | var server; 11 | 12 | 13 | module('Acceptance: CRUD Failure', function(hooks) { 14 | setupApplicationTest(hooks); 15 | 16 | hooks.beforeEach(function() { 17 | store = this.owner.lookup('service:store'); 18 | 19 | server = new Pretender(function() { 20 | 21 | // Permission denied error 22 | this.get('/test-api/posts/1/', function() { 23 | return [401, {'Content-Type': 'application/json'}, JSON.stringify({detail: 'Authentication credentials were not provided.'})]; 24 | }); 25 | 26 | // Server error 27 | this.get('/test-api/posts/2/', function() { 28 | // This is the default error page for Django when DEBUG is set to False. 29 | return [500, {'Content-Type': 'application/json'}, JSON.stringify({detail: 'Something bad'})]; 30 | }); 31 | 32 | // Authentication Invalid error 33 | this.post('/test-api/posts/3', function() { 34 | return [400, {'Content-Type': 'application/json'}, JSON.stringify({name: 'error 1', non_field_errors: 'error 2'})]; 35 | }); 36 | 37 | 38 | // Create field errors 39 | this.post('/test-api/posts/', function(request) { 40 | var data = JSON.parse(request.requestBody); 41 | if (data.body === 'non_field_errors') { 42 | return [400, {'Content-Type': 'application/json'}, JSON.stringify({ 43 | body: ['error 1'], 44 | non_field_errors: ['error 2', 'error 3'] 45 | })]; 46 | } 47 | return [400, {'Content-Type': 'application/json'}, JSON.stringify({ 48 | post_title: ['This field is required.'], 49 | body: ['This field is required.', 'This field cannot be blank.'] 50 | })]; 51 | }); 52 | 53 | // Create nested field errors 54 | this.post('/test-api/embedded-post-comments/', function() { 55 | return [400, {'Content-Type': 'application/json'}, JSON.stringify({ 56 | post: { 57 | post_title: ['This field is required.'], 58 | body: ['This field is required.', 'This field cannot be blank.'] 59 | } 60 | })]; 61 | }); 62 | 63 | // Update field errors 64 | this.get('/test-api/posts/3/', function() { 65 | return [200, {'Content-Type': 'application/json'}, JSON.stringify({ 66 | id: 3, 67 | post_title: 'post title 3', 68 | body: 'post body 3', 69 | comments: [] 70 | })]; 71 | }); 72 | this.put('/test-api/posts/3/', function() { 73 | return [400, {'Content-Type': 'application/json'}, JSON.stringify({ 74 | post_title: ['Ensure this value has at most 50 characters (it has 53).'], 75 | body: ['This field is required.'] 76 | })]; 77 | }); 78 | }); 79 | }); 80 | 81 | hooks.afterEach(function() { 82 | server.shutdown(); 83 | }); 84 | 85 | test('Permission denied error', function(assert) { 86 | assert.expect(4); 87 | 88 | return run(function() { 89 | 90 | return store.findRecord('post', 1).then({}, function(response) { 91 | const error = response.errors[0]; 92 | 93 | assert.ok(error); 94 | assert.equal(error.status, 401); 95 | assert.equal(error.detail, 'Authentication credentials were not provided.'); 96 | assert.equal(response.message, 'Unauthorized'); 97 | }); 98 | }); 99 | }); 100 | 101 | test('Server error', function(assert) { 102 | assert.expect(4); 103 | 104 | return run(function() { 105 | 106 | return store.findRecord('post', 2).then({}, function(response) { 107 | const error = response.errors[0]; 108 | 109 | assert.ok(response); 110 | assert.equal(error.status, 500); 111 | assert.equal(error.detail, 'Something bad'); 112 | assert.equal(response.message, 'Internal Server Error'); 113 | }); 114 | }); 115 | }); 116 | 117 | test('Invalid with non field errors', function(assert) { 118 | return run(function() { 119 | 120 | var post = store.createRecord('post', { 121 | postTitle: '', 122 | body: 'non_field_errors' 123 | }); 124 | 125 | return post.save().then({}, function(response) { 126 | const bodyErrors = post.get('errors.body'), 127 | nonFieldErrors1 = response.errors[1], 128 | nonFieldErrors2 = response.errors[2]; 129 | assert.ok(response); 130 | assert.ok(response.errors); 131 | assert.equal(post.get('isValid'), false); 132 | 133 | assert.equal(bodyErrors.length, 1); 134 | assert.equal(bodyErrors[0].message, 'error 1'); 135 | 136 | assert.equal(nonFieldErrors1.detail, 'error 2'); 137 | assert.equal(nonFieldErrors1.source.pointer, '/data'); 138 | assert.equal(nonFieldErrors1.title, 'Validation Error'); 139 | 140 | assert.equal(nonFieldErrors2.detail, 'error 3'); 141 | assert.equal(nonFieldErrors2.source.pointer, '/data'); 142 | assert.equal(nonFieldErrors2.title, 'Validation Error'); 143 | 144 | }); 145 | }); 146 | }); 147 | 148 | test('Create field errors', function(assert) { 149 | assert.expect(8); 150 | 151 | return run(function() { 152 | 153 | var post = store.createRecord('post', { 154 | postTitle: '', 155 | body: '' 156 | }); 157 | 158 | return post.save().then({}, function(response) { 159 | const postTitleErrors = post.get('errors.postTitle'), 160 | bodyErrors = post.get('errors.body'); 161 | assert.ok(response); 162 | assert.ok(response.errors); 163 | assert.equal(post.get('isValid'), false); 164 | 165 | // Test camelCase field. 166 | assert.equal(postTitleErrors.length, 1); 167 | assert.equal(postTitleErrors[0].message, 'This field is required.'); 168 | 169 | // Test non-camelCase field. 170 | assert.equal(bodyErrors.length, 2); 171 | assert.equal(bodyErrors[0].message, 'This field is required.'); 172 | assert.equal(bodyErrors[1].message, 'This field cannot be blank.'); 173 | }); 174 | }); 175 | }); 176 | 177 | test('Created nested field errors', function(assert) { 178 | assert.expect(8); 179 | 180 | return run(function() { 181 | var post = store.createRecord('post'); 182 | var embeddedPostComment = store.createRecord('embedded-post-comment', { 183 | body: 'This is my new comment', 184 | post: post 185 | }); 186 | 187 | return embeddedPostComment.save().then(null, function(response) { 188 | const comment = embeddedPostComment; 189 | assert.ok(response); 190 | assert.ok(response.errors); 191 | assert.notOk(comment.get('isValid')); 192 | 193 | /* 194 | JSON API technically does not allow nesting, so the nested errors are 195 | processed as if they were attributes on the comment itself. As a result, 196 | comment.get('post.isValid') returns true :-( 197 | 198 | This assertion will fail: 199 | assert.notOk(comment.get('post.isValid')); 200 | 201 | Related discussion: https://github.com/json-api/json-api/issues/899 202 | */ 203 | 204 | let postBodyErrors = comment.get('errors.post/body'); 205 | let postPostTitleErrors = comment.get('errors.post/post_title'); 206 | 207 | assert.equal(postBodyErrors.length, 2); 208 | assert.equal(postBodyErrors[0].message, 'This field is required.'); 209 | assert.equal(postBodyErrors[1].message, 'This field cannot be blank.'); 210 | assert.equal(postPostTitleErrors.length, 1); 211 | assert.equal(postPostTitleErrors[0].message, 'This field is required.'); 212 | }); 213 | }); 214 | }); 215 | 216 | test('Update field errors', function(assert) { 217 | assert.expect(9); 218 | 219 | return run(function() { 220 | 221 | return store.findRecord('post', 3).then(function(post) { 222 | assert.ok(post); 223 | assert.equal(post.get('hasDirtyAttributes'), false); 224 | post.set('postTitle', 'Lorem ipsum dolor sit amet, consectetur adipiscing el'); 225 | post.set('body', ''); 226 | assert.equal(post.get('hasDirtyAttributes'), true); 227 | 228 | post.save().then({}, function(response) { 229 | const postTitleErrors = post.get('errors.postTitle'), 230 | bodyErrors = post.get('errors.body'); 231 | 232 | assert.ok(response); 233 | assert.ok(response.errors); 234 | 235 | // Test camelCase field. 236 | assert.equal(postTitleErrors.length, 1); 237 | assert.equal(postTitleErrors[0].message, 'Ensure this value has at most 50 characters (it has 53).'); 238 | 239 | // Test non-camelCase field. 240 | assert.equal(bodyErrors.length, 1); 241 | assert.equal(bodyErrors[0].message, 'This field is required.'); 242 | }); 243 | }); 244 | }); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /tests/acceptance/crud-success-test.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { merge } from '@ember/polyfills'; 3 | import $ from 'jquery'; 4 | import { 5 | module, 6 | test 7 | } from 'qunit'; 8 | import { setupApplicationTest } from 'ember-qunit'; 9 | import Pretender from 'pretender'; 10 | 11 | var store; 12 | var server; 13 | 14 | var posts = [ 15 | { 16 | id: 1, 17 | post_title: 'post title 1', 18 | body: 'post body 1', 19 | comments: [] 20 | }, 21 | { 22 | id: 2, 23 | post_title: 'post title 2', 24 | body: 'post body 2', 25 | comments: [] 26 | }, 27 | { 28 | id: 3, 29 | post_title: 'post title 3', 30 | body: 'post body 3', 31 | comments: [] 32 | } 33 | ]; 34 | 35 | module('Acceptance: CRUD Success', function(hooks) { 36 | setupApplicationTest(hooks); 37 | 38 | hooks.beforeEach(function() { 39 | store = this.owner.lookup('service:store'); 40 | 41 | server = new Pretender(function() { 42 | 43 | // Retrieve list of non-paginated records 44 | this.get('/test-api/posts/', function(request) { 45 | if (request.queryParams.post_title === 'post title 2') { 46 | return [200, {'Content-Type': 'application/json'}, JSON.stringify([posts[1]])]; 47 | } else { 48 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(posts)]; 49 | } 50 | }); 51 | 52 | // Retrieve single record 53 | this.get('/test-api/posts/1/', function() { 54 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(posts[0])]; 55 | }); 56 | 57 | // Create record 58 | this.post('/test-api/posts/', function(request) { 59 | var data = $.parseJSON(request.requestBody); 60 | return [201, {'Content-Type': 'application/json'}, JSON.stringify(data)]; 61 | }); 62 | 63 | // Update record 64 | this.put('/test-api/posts/1/', function(request) { 65 | var data = merge(posts[0], $.parseJSON(request.requestBody)); 66 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(data)]; 67 | }); 68 | 69 | // Delete record 70 | this.delete('/test-api/posts/1/', function() { 71 | return [204]; 72 | }); 73 | }); 74 | }); 75 | 76 | hooks.afterEach(function() { 77 | server.shutdown(); 78 | }); 79 | 80 | test('Retrieve list of non-paginated records', function(assert) { 81 | assert.expect(4); 82 | 83 | return store.findAll('post').then(function(posts) { 84 | 85 | assert.ok(posts); 86 | assert.equal(posts.get('length'), 3); 87 | 88 | var post = posts.objectAt(2); 89 | 90 | assert.equal(post.get('postTitle'), 'post title 3'); 91 | assert.equal(post.get('body'), 'post body 3'); 92 | }); 93 | }); 94 | 95 | test('Retrieve single record with findRecord', function(assert) { 96 | assert.expect(3); 97 | 98 | return run(function() { 99 | 100 | return store.findRecord('post', 1).then(function(post) { 101 | assert.ok(post); 102 | assert.equal(post.get('postTitle'), 'post title 1'); 103 | assert.equal(post.get('body'), 'post body 1'); 104 | }); 105 | }); 106 | }); 107 | 108 | test('Retrieve single record with queryRecord', function(assert) { 109 | assert.expect(3); 110 | 111 | return run(function() { 112 | 113 | return store.queryRecord('post', { slug: 'post-title-1' }).then(function(post) { 114 | 115 | assert.ok(post); 116 | assert.equal(post.get('postTitle'), 'post title 1'); 117 | assert.equal(post.get('body'), 'post body 1'); 118 | }); 119 | }); 120 | }); 121 | 122 | test('Retrieve via query', function(assert) { 123 | assert.expect(3); 124 | 125 | return run(function() { 126 | 127 | return store.query('post', {post_title: 'post title 2'}).then(function(post) { 128 | 129 | assert.ok(post); 130 | 131 | post = post.objectAt(0); 132 | assert.equal(post.get('postTitle'), 'post title 2'); 133 | assert.equal(post.get('body'), 'post body 2'); 134 | }); 135 | }); 136 | }); 137 | 138 | test('Create record', function(assert) { 139 | assert.expect(5); 140 | 141 | return run(function() { 142 | 143 | var post = store.createRecord('post', { 144 | id: 4, 145 | postTitle: 'my new post title', 146 | body: 'my new post body' 147 | }); 148 | 149 | return post.save().then(function(post) { 150 | 151 | assert.ok(post); 152 | assert.equal(post.get('id'), 4); 153 | assert.equal(post.get('postTitle'), 'my new post title'); 154 | assert.equal(post.get('body'), 'my new post body'); 155 | 156 | var requestBody = (JSON.parse(server.handledRequests.pop().requestBody)); 157 | assert.equal(requestBody.id, 4); 158 | 159 | }); 160 | }); 161 | }); 162 | 163 | test('Update record', function(assert) { 164 | assert.expect(7); 165 | 166 | return run(function() { 167 | 168 | return store.findRecord('post', 1).then(function(post) { 169 | 170 | assert.ok(post); 171 | assert.equal(post.get('hasDirtyAttributes'), false); 172 | 173 | return run(function() { 174 | 175 | post.set('postTitle', 'new post title'); 176 | post.set('body', 'new post body'); 177 | assert.equal(post.get('hasDirtyAttributes'), true); 178 | 179 | return post.save().then(function(post) { 180 | 181 | assert.ok(post); 182 | assert.equal(post.get('hasDirtyAttributes'), false); 183 | assert.equal(post.get('postTitle'), 'new post title'); 184 | assert.equal(post.get('body'), 'new post body'); 185 | }); 186 | }); 187 | }); 188 | }); 189 | }); 190 | 191 | test('Delete record', function(assert) { 192 | assert.expect(2); 193 | 194 | return run(function() { 195 | 196 | return store.findRecord('post', 1).then(function(post) { 197 | 198 | assert.ok(post); 199 | 200 | return post.destroyRecord().then(function(post) { 201 | 202 | assert.ok(post); 203 | }); 204 | }); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /tests/acceptance/embedded-records-test.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import $ from 'jquery'; 3 | import { 4 | module, 5 | test 6 | } from 'qunit'; 7 | import { setupApplicationTest } from 'ember-qunit'; 8 | import Pretender from 'pretender'; 9 | 10 | var store; 11 | var server; 12 | 13 | var embeddedCommentsPosts = [ 14 | { 15 | id: 1, 16 | post_title: 'post title 1', 17 | body: 'post body 1', 18 | comments: [ 19 | { 20 | id: 2, 21 | body: 'comment body 2' 22 | }, 23 | { 24 | id: 3, 25 | body: 'comment body 3' 26 | }, 27 | { 28 | id: 4, 29 | body: 'comment body 4' 30 | } 31 | ] 32 | } 33 | ]; 34 | 35 | var embeddedPostComments = [ 36 | { 37 | id: 5, 38 | body: 'comment body 5', 39 | post: { 40 | id: 6, 41 | post_title: 'post title 6', 42 | body: 'post body 6' 43 | } 44 | } 45 | ]; 46 | 47 | var posts = [ 48 | { 49 | id: 7, 50 | post_title: 'post title 7', 51 | body: 'post body 7', 52 | comments: [] 53 | } 54 | ]; 55 | 56 | module('Acceptance: Embedded Records', function(hooks) { 57 | setupApplicationTest(hooks); 58 | 59 | hooks.beforeEach(function() { 60 | store = this.owner.lookup('service:store'); 61 | 62 | server = new Pretender(function() { 63 | 64 | this.get('/test-api/embedded-comments-posts/1/', function( ) { 65 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(embeddedCommentsPosts[0])]; 66 | }); 67 | 68 | this.get('/test-api/embedded-post-comments/5/', function() { 69 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(embeddedPostComments[0])]; 70 | }); 71 | 72 | this.get('/test-api/posts/7/', function() { 73 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(posts[0])]; 74 | }); 75 | 76 | this.post('/test-api/embedded-post-comments/', function(request) { 77 | let data = $.parseJSON(request.requestBody); 78 | data['id'] = 8; 79 | data['post'] = posts[0]; 80 | return [201, {'Content-Type': 'application/json'}, JSON.stringify(data)]; 81 | }); 82 | 83 | }); 84 | }); 85 | 86 | hooks.afterEach(function() { 87 | server.shutdown(); 88 | }); 89 | 90 | test('belongsTo retrieve', function(assert) { 91 | assert.expect(6); 92 | 93 | return run(function() { 94 | 95 | return store.findRecord('embedded-post-comment', 5).then(function(comment) { 96 | assert.ok(comment); 97 | assert.equal(comment.get('body'), 'comment body 5'); 98 | 99 | let post = comment.get('post'); 100 | assert.ok(post); 101 | assert.equal(post.get('postTitle'), 'post title 6'); 102 | assert.equal(post.get('body'), 'post body 6'); 103 | 104 | assert.equal(server.handledRequests.length, 1); 105 | }); 106 | }); 107 | }); 108 | 109 | test('hasMany retrieve', function(assert) { 110 | assert.expect(12); 111 | 112 | return run(function() { 113 | 114 | return store.findRecord('embedded-comments-post', 1).then(function(post) { 115 | assert.ok(post); 116 | assert.equal(post.get('postTitle'), 'post title 1'); 117 | assert.equal(post.get('body'), 'post body 1'); 118 | 119 | let comments = post.get('comments'); 120 | assert.ok(comments); 121 | assert.equal(comments.get('length'), 3); 122 | assert.ok(comments.objectAt(0)); 123 | assert.equal(comments.objectAt(0).get('body'), 'comment body 2'); 124 | assert.ok(comments.objectAt(1)); 125 | assert.equal(comments.objectAt(1).get('body'), 'comment body 3'); 126 | assert.ok(comments.objectAt(2)); 127 | assert.equal(comments.objectAt(2).get('body'), 'comment body 4'); 128 | 129 | assert.equal(server.handledRequests.length, 1); 130 | }); 131 | }); 132 | }); 133 | 134 | test('belongsTo create', function(assert) { 135 | assert.expect(6); 136 | 137 | return run(function() { 138 | 139 | return store.findRecord('post', 7).then(function(post) { 140 | 141 | let comment = store.createRecord('embedded-post-comment', { 142 | body: 'comment body 9', 143 | post: post 144 | }); 145 | 146 | return comment.save().then(function(comment) { 147 | 148 | assert.ok(comment); 149 | assert.ok(comment.get('id')); 150 | assert.equal(comment.get('body'), 'comment body 9'); 151 | assert.ok(comment.get('post')); 152 | 153 | assert.equal(server.handledRequests.length, 2); 154 | let requestBody = (JSON.parse(server.handledRequests.pop().requestBody)); 155 | assert.equal(requestBody.post, 7); 156 | }); 157 | }); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /tests/acceptance/pagination-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | module, 3 | test 4 | } from 'qunit'; 5 | import { setupApplicationTest } from 'ember-qunit'; 6 | import Pretender from 'pretender'; 7 | 8 | var store; 9 | var server; 10 | 11 | var posts = [ 12 | { 13 | id: 1, 14 | post_title: 'post title 1', 15 | body: 'post body 1', 16 | comments: [] 17 | }, 18 | { 19 | id: 2, 20 | post_title: 'post title 2', 21 | body: 'post body 2', 22 | comments: [] 23 | }, 24 | { 25 | id: 3, 26 | post_title: 'post title 3', 27 | body: 'post body 3', 28 | comments: [] 29 | }, 30 | { 31 | id: 4, 32 | post_title: 'post title 4', 33 | body: 'post body 4', 34 | comments: [] 35 | }, 36 | { 37 | id: 5, 38 | post_title: 'post title 5', 39 | body: 'post body 5', 40 | comments: [] 41 | }, 42 | { 43 | id: 6, 44 | post_title: 'post title 6', 45 | body: 'post body 6', 46 | comments: [] 47 | } 48 | ]; 49 | 50 | module('Acceptance: Pagination', function(hooks) { 51 | setupApplicationTest(hooks); 52 | 53 | hooks.beforeEach(function() { 54 | store = this.owner.lookup('service:store'); 55 | 56 | // The implementation of the paginated Pretender server is dynamic 57 | // so it can be used with all of the pagination tests. Otherwise, 58 | // different urls would need to be used which would require new 59 | // models. 60 | server = new Pretender(function() { 61 | this.get('/test-api/posts/', function(request) { 62 | var page = 1, 63 | pageSize = 4; 64 | 65 | if (request.queryParams.page_size !== undefined) { 66 | pageSize = Number(request.queryParams.page_size).valueOf(); 67 | } 68 | 69 | var maxPages = posts.length / pageSize; 70 | 71 | if (posts.length % pageSize > 0) { 72 | maxPages++; 73 | } 74 | 75 | if (request.queryParams.page !== undefined) { 76 | page = Number(request.queryParams.page).valueOf(); 77 | if (page > maxPages) { 78 | return [404, {'Content-Type': 'text/html'}, '

Page not found

']; 79 | } 80 | } 81 | 82 | var nextPage = page + 1; 83 | var nextUrl = null; 84 | if (nextPage <= maxPages) { 85 | nextUrl = '/test-api/posts/?page=' + nextPage; 86 | } 87 | 88 | var previousPage = page - 1; 89 | var previousUrl = null; 90 | if (previousPage > 1) { 91 | previousUrl = '/test-api/posts/?page=' + previousPage; 92 | } else if (previousPage === 1) { 93 | // The DRF previous URL doesn't always include the page=1 query param in the results for page 2. 94 | previousUrl = '/test-api/posts/'; 95 | } 96 | 97 | var offset = (page - 1) * pageSize; 98 | return [200, {'Content-Type': 'application/json'}, JSON.stringify({ 99 | count: posts.length, 100 | next: nextUrl, 101 | previous: previousUrl, 102 | results: posts.slice(offset, offset + pageSize) 103 | })]; 104 | }); 105 | }); 106 | }); 107 | 108 | hooks.afterEach(function() { 109 | server.shutdown(); 110 | }); 111 | 112 | test('Retrieve list of paginated records', function(assert) { 113 | assert.expect(7); 114 | 115 | return store.query('post', {page: 1}).then(function(response) { 116 | assert.ok(response); 117 | 118 | assert.equal(response.get('length'), 4); 119 | 120 | // Test the camelCase and non-camelCase fields of a paginated result. 121 | var post = response.objectAt(1); 122 | assert.equal(post.get('postTitle'), 'post title 2'); 123 | assert.equal(post.get('body'), 'post body 2'); 124 | 125 | var metadata = response.get('meta'); 126 | assert.equal(metadata.count, 6); 127 | assert.equal(metadata.next, 2); 128 | assert.equal(metadata.previous, null); 129 | }); 130 | }); 131 | 132 | test('queryRecord with paginated results returns a single record', function(assert) { 133 | return store.queryRecord('post', { title: 'post title 1' }).then(function(post) { 134 | assert.ok(post); 135 | assert.equal(post.get('postTitle'), 'post title 1'); 136 | assert.equal(post.get('body'), 'post body 1'); 137 | }); 138 | }); 139 | 140 | test("Type metadata doesn't have previous", function(assert) { 141 | assert.expect(4); 142 | 143 | return store.query('post', {page: 1}).then(function(response) { 144 | assert.ok(response); 145 | 146 | var metadata = response.get('meta'); 147 | assert.equal(metadata.count, 6); 148 | assert.equal(metadata.next, 2); 149 | assert.equal(metadata.previous, null); 150 | }); 151 | }); 152 | 153 | 154 | test("Type metadata doesn't have next", function(assert) { 155 | assert.expect(5); 156 | 157 | return store.query('post', {page: 2}).then(function(response) { 158 | assert.ok(response); 159 | assert.equal(response.get('length'), 2); 160 | 161 | var metadata = response.get('meta'); 162 | assert.equal(metadata.count, 6); 163 | assert.equal(metadata.next, null); 164 | assert.equal(metadata.previous, 1); 165 | }); 166 | }); 167 | 168 | 169 | test("Test page_size query param", function(assert) { 170 | assert.expect(5); 171 | 172 | return store.query('post', {page: 2, page_size: 2}).then(function(response) { 173 | assert.ok(response); 174 | assert.equal(response.get('length'), 2); 175 | 176 | var metadata = response.get('meta'); 177 | assert.equal(metadata.count, 6); 178 | assert.equal(metadata.previous, 1); 179 | assert.equal(metadata.next, 3); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /tests/acceptance/relationship-links-test.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { 3 | module, 4 | test 5 | } from 'qunit'; 6 | import { setupApplicationTest } from 'ember-qunit'; 7 | import Pretender from 'pretender'; 8 | 9 | var store; 10 | var server; 11 | 12 | var posts = [ 13 | { 14 | id: 1, 15 | post_title: 'post title 1', 16 | body: 'post body 1', 17 | comments: '/test-api/posts/1/comments/' 18 | }, 19 | { 20 | id: 2, 21 | post_title: 'post title 2', 22 | body: 'post body 2', 23 | comments: '/test-api/posts/2/comments/' 24 | } 25 | ]; 26 | 27 | var comments = [ 28 | { 29 | id: 1, 30 | body: 'comment body 1', 31 | post: '/test-api/posts/1/' 32 | }, 33 | { 34 | id: 2, 35 | body: 'comment body 1', 36 | post: '/test-api/posts/2/' 37 | }, 38 | { 39 | id: 3, 40 | body: 'comment body 2', 41 | post: '/test-api/posts/1/' 42 | }, 43 | { 44 | id: 4, 45 | body: 'comment body 3', 46 | post: '/test-api/posts/1/' 47 | } 48 | ]; 49 | 50 | module('Acceptance: Relationship Links', function(hooks) { 51 | setupApplicationTest(hooks); 52 | 53 | hooks.beforeEach(function() { 54 | store = this.owner.lookup('service:store'); 55 | 56 | server = new Pretender(function() { 57 | this.get('/test-api/posts/:id/', function(request) { 58 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(posts[request.params.id - 1])]; 59 | }); 60 | 61 | this.get('/test-api/posts/:id/comments/', function(request) { 62 | var related_post_url = '/test-api/posts/' + request.params.id + '/'; 63 | var related_comments = comments.filter(function(comment) { 64 | return comment.post === related_post_url; 65 | }); 66 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(related_comments)]; 67 | }); 68 | 69 | this.get('/test-api/comments/:id/', function(request) { 70 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(comments[request.params.id - 1])]; 71 | }); 72 | }); 73 | }); 74 | 75 | hooks.afterEach(function() { 76 | server.shutdown(); 77 | }); 78 | 79 | test('belongsTo', function(assert) { 80 | assert.expect(2); 81 | 82 | return run(function() { 83 | 84 | return store.findRecord('comment', 2).then(function(comment) { 85 | 86 | assert.ok(comment); 87 | 88 | return comment.get('post').then(function(post) { 89 | assert.ok(post); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | test('hasMany', function(assert) { 96 | assert.expect(9); 97 | 98 | return run(function() { 99 | 100 | return store.findRecord('post', 1).then(function(post) { 101 | 102 | assert.ok(post); 103 | 104 | return post.get('comments').then(function(related_comments) { 105 | assert.ok(related_comments); 106 | assert.equal(related_comments.get('length'), 3); 107 | assert.ok(related_comments.objectAt(0)); 108 | assert.equal(related_comments.objectAt(0).id, comments[0].id); 109 | assert.ok(related_comments.objectAt(1)); 110 | assert.equal(related_comments.objectAt(1).id, comments[2].id); 111 | assert.ok(related_comments.objectAt(2)); 112 | assert.equal(related_comments.objectAt(2).id, comments[3].id); 113 | }); 114 | }); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /tests/acceptance/relationships-test.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { 3 | module, 4 | test 5 | } from 'qunit'; 6 | import { setupApplicationTest } from 'ember-qunit'; 7 | import Pretender from 'pretender'; 8 | 9 | var store; 10 | var server; 11 | 12 | var posts = [ 13 | { 14 | id: 1, 15 | post_title: 'post title 1', 16 | body: 'post body 1', 17 | comments: [1, 2, 3] 18 | }, 19 | { 20 | id: 2, 21 | post_title: 'post title 2', 22 | body: 'post body 2', 23 | comments: [4] 24 | } 25 | ]; 26 | 27 | var comments = [ 28 | { 29 | id: 1, 30 | body: 'comment body 1', 31 | post: 1 32 | }, 33 | { 34 | id: 2, 35 | body: 'comment body 2', 36 | post: 1 37 | }, 38 | { 39 | id: 3, 40 | body: 'comment body 3', 41 | post: 1 42 | }, 43 | { 44 | id: 4, 45 | body: 'comment body 4', 46 | post: 2 47 | } 48 | ]; 49 | 50 | module('Acceptance: Relationships', function(hooks) { 51 | setupApplicationTest(hooks); 52 | 53 | hooks.beforeEach(function() { 54 | store = this.owner.lookup('service:store'); 55 | 56 | server = new Pretender(function() { 57 | 58 | this.get('/test-api/posts/:id/', function(request) { 59 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(posts[request.params.id - 1])]; 60 | }); 61 | 62 | this.get('/test-api/comments/:id/', function(request) { 63 | return [200, {'Content-Type': 'application/json'}, JSON.stringify(comments[request.params.id - 1])]; 64 | }); 65 | }); 66 | }); 67 | 68 | hooks.afterEach(function() { 69 | server.shutdown(); 70 | }); 71 | 72 | test('belongsTo', function(assert) { 73 | assert.expect(2); 74 | 75 | return run(function() { 76 | 77 | return store.findRecord('comment', 2).then(function(comment) { 78 | 79 | assert.ok(comment); 80 | 81 | return comment.get('post').then(function(post) { 82 | assert.ok(post); 83 | }); 84 | }); 85 | }); 86 | }); 87 | 88 | test('hasMany', function(assert) { 89 | assert.expect(6); 90 | 91 | return run(function() { 92 | 93 | return store.findRecord('post', 1).then(function(post) { 94 | 95 | assert.ok(post); 96 | 97 | return post.get('comments').then(function(comments) { 98 | assert.ok(comments); 99 | assert.equal(comments.get('length'), 3); 100 | assert.ok(comments.objectAt(0)); 101 | assert.ok(comments.objectAt(1)); 102 | assert.ok(comments.objectAt(2)); 103 | }); 104 | }); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /tests/dummy/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser" : true, 8 | "boss" : true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": false 32 | } 33 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/comment.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | body: DS.attr(), 5 | post: DS.belongsTo('post', {async: true}) 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/embedded-comments-post.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | postTitle: DS.attr(), 5 | body: DS.attr(), 6 | comments: DS.hasMany('comment', {async: false}) 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/models/embedded-post-comment.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | body: DS.attr(), 5 | post: DS.belongsTo('post', {async: false}) 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/post.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | postTitle: DS.attr(), 5 | body: DS.attr(), 6 | comments: DS.hasMany('comment', {async: true}) 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/serializers/embedded-comments-post.js: -------------------------------------------------------------------------------- 1 | import DRFSerializer from './drf'; 2 | import DS from 'ember-data'; 3 | 4 | export default DRFSerializer.extend(DS.EmbeddedRecordsMixin, { 5 | attrs: { 6 | comments: {embedded: 'always'} 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/embedded-post-comment.js: -------------------------------------------------------------------------------- 1 | import DRFSerializer from './drf'; 2 | import DS from 'ember-data'; 3 | 4 | export default DRFSerializer.extend(DS.EmbeddedRecordsMixin, { 5 | attrs: { 6 | post: {serialize: 'id', deserialize: 'records'} 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/styles/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/templates/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Ember

2 | 3 | {{outlet}} 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/app/views/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | 24 | // The integration tests don't work with the API_HOST setting set 25 | // because Pretender doesn't work when a host set. 26 | API_HOST: '', 27 | API_NAMESPACE: 'test-api' 28 | } 29 | }; 30 | 31 | if (environment === 'development') { 32 | // ENV.APP.LOG_RESOLVER = true; 33 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 34 | // ENV.APP.LOG_TRANSITIONS = true; 35 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 36 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 37 | } 38 | 39 | if (environment === 'test') { 40 | // Testem prefers this... 41 | ENV.locationType = 'none'; 42 | 43 | // keep test console output quieter 44 | ENV.APP.LOG_ACTIVE_GENERATION = false; 45 | ENV.APP.LOG_VIEW_LOOKUPS = false; 46 | 47 | ENV.APP.rootElement = '#ember-testing'; 48 | ENV.APP.autoboot = false; 49 | } 50 | 51 | if (environment === 'production') { 52 | // here you can enable a production-specific feature 53 | } 54 | 55 | return ENV; 56 | }; 57 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/dummy/public/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/adapters/drf-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | 4 | module('DRFAdapter', function(hooks) { 5 | setupTest(hooks); 6 | 7 | hooks.beforeEach(function() { 8 | this.adapter = this.owner.lookup('adapter:application'); 9 | this.adapter.set('host', 'test-host'); 10 | }); 11 | 12 | test('host config override', function(assert) { 13 | var adapter = this.adapter; 14 | assert.equal(adapter.get('host'), 'test-host'); 15 | }); 16 | 17 | test('namespace config override', function(assert) { 18 | var adapter = this.adapter; 19 | assert.equal(adapter.get('namespace'), 'test-api'); 20 | }); 21 | 22 | test('pathForType', function(assert) { 23 | var adapter = this.adapter; 24 | assert.equal(adapter.pathForType('Animal'), 'animals'); 25 | assert.equal(adapter.pathForType('FurryAnimals'), 'furry-animals'); 26 | }); 27 | 28 | test('buildURL', function(assert) { 29 | var adapter = this.adapter; 30 | assert.equal(adapter.buildURL('Animal', 5, null), 'test-host/test-api/animals/5/'); 31 | assert.equal(adapter.buildURL('FurryAnimals', 5, null), 'test-host/test-api/furry-animals/5/'); 32 | assert.equal(adapter.buildURL('Animal', null, null, 'query', { limit: 10 }), 'test-host/test-api/animals/'); 33 | }); 34 | 35 | test('buildURL - no trailing slashes', function(assert) { 36 | var adapter = this.adapter; 37 | adapter.set('addTrailingSlashes', false); 38 | assert.equal(adapter.buildURL('Animal', 5, null), 'test-host/test-api/animals/5'); 39 | assert.equal(adapter.buildURL('FurryAnimals', 5, null), 'test-host/test-api/furry-animals/5'); 40 | assert.equal(adapter.buildURL('Animal', null, null, 'query', { limit: 10 }), 'test-host/test-api/animals'); 41 | }); 42 | 43 | test('handleResponse - returns invalid error if 400 response', function(assert) { 44 | const headers = {}, 45 | status = 400, 46 | payload = { 47 | name: ['This field cannot be blank.'], 48 | post_title: ['This field cannot be blank.', 'This field cannot be empty.'] 49 | }; 50 | 51 | var adapter = this.adapter; 52 | var error = adapter.handleResponse(status, headers, payload); 53 | assert.equal(error.errors[0].detail, 'This field cannot be blank.'); 54 | assert.equal(error.errors[0].source.pointer, '/data/attributes/name'); 55 | assert.equal(error.errors[0].title, 'Invalid Attribute'); 56 | 57 | assert.equal(error.errors[1].detail, 'This field cannot be blank.'); 58 | assert.equal(error.errors[1].source.pointer, '/data/attributes/post_title'); 59 | assert.equal(error.errors[1].title, 'Invalid Attribute'); 60 | 61 | assert.equal(error.errors[2].detail, 'This field cannot be empty.'); 62 | assert.equal(error.errors[2].source.pointer, '/data/attributes/post_title'); 63 | assert.equal(error.errors[2].title, 'Invalid Attribute'); 64 | }); 65 | 66 | test('handleResponse - returns error if not 400 response', function(assert) { 67 | const headers = {}, 68 | status = 403, 69 | payload = { detail: 'You do not have permission to perform this action.'}, 70 | adapter = this.adapter; 71 | var error = adapter.handleResponse(status, headers, payload); 72 | assert.equal(error.errors[0].detail, payload.detail); 73 | }); 74 | 75 | test('handleResponse - returns error if payload is empty', function(assert) { 76 | const headers = {}, 77 | status = 409, 78 | payload = {}, 79 | adapter = this.adapter; 80 | var error = adapter.handleResponse(status, headers, payload); 81 | assert.equal(error.errors[0].detail, ''); 82 | }); 83 | 84 | test('handleResponse - returns error with internal server error if 500', function(assert) { 85 | const headers = {}, 86 | status = 500, 87 | payload = {}, 88 | adapter = this.adapter; 89 | var error = adapter.handleResponse(status, headers, payload); 90 | assert.equal(error.errors[0].detail, ''); 91 | assert.equal(error.message, 'Internal Server Error'); 92 | }); 93 | 94 | test('_stripIDFromURL - returns base URL for type', function(assert) { 95 | var snapshot = { 96 | modelName: 'furry-animal' 97 | }; 98 | var adapter = this.adapter; 99 | 100 | assert.equal(adapter._stripIDFromURL('store', snapshot), 'test-host/test-api/furry-animals/'); 101 | }); 102 | 103 | test('_stripIDFromURL without trailing slash - returns base URL for type', function(assert) { 104 | var snapshot = { 105 | modelName: 'furry-animal' 106 | }; 107 | var adapter = this.adapter; 108 | adapter.set('addTrailingSlashes', false); 109 | 110 | assert.equal(adapter._stripIDFromURL('store', snapshot), 'test-host/test-api/furry-animals'); 111 | }); 112 | 113 | test('_formatPayload returns array when string received', function(assert) { 114 | var payload = { 115 | key: 'value' 116 | }; 117 | var adapter = this.adapter; 118 | 119 | assert.deepEqual(adapter._formatPayload(payload), { 120 | key: ['value'] 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/unit/serializers/drf-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupTest } from 'ember-qunit'; 3 | import sinon from 'sinon'; 4 | 5 | module('DRFSerializer', function(hooks) { 6 | setupTest(hooks); 7 | 8 | test('serializeIntoHash', function(assert) { 9 | var serializer = this.owner.lookup('serializer:application'); 10 | serializer.serialize = sinon.stub().returns({serialized: 'record'}); 11 | var hash = {existing: 'hash'}; 12 | 13 | serializer.serializeIntoHash(hash, 'type', 'record', 'options'); 14 | 15 | assert.ok(serializer.serialize.calledWith( 16 | 'record', 'options' 17 | ), 'serialize not called properly'); 18 | assert.deepEqual(hash, {serialized: 'record', existing: 'hash'}); 19 | }); 20 | 21 | test('keyForAttribute', function(assert) { 22 | var serializer = this.owner.lookup('serializer:application'); 23 | 24 | var result = serializer.keyForAttribute('firstName'); 25 | 26 | assert.equal(result, 'first_name'); 27 | }); 28 | 29 | test('keyForRelationship', function(assert) { 30 | var serializer = this.owner.lookup('serializer:application'); 31 | 32 | var result = serializer.keyForRelationship('projectManagers', 'hasMany'); 33 | 34 | assert.equal(result, 'project_managers'); 35 | }); 36 | 37 | test('extractPageNumber', function(assert) { 38 | var serializer = this.owner.lookup('serializer:application'); 39 | 40 | assert.equal(serializer.extractPageNumber('http://xmpl.com/a/p/?page=3234'), 3234, 41 | 'extractPageNumber failed on absolute URL'); 42 | 43 | assert.equal(serializer.extractPageNumber('/a/p/?page=3234'), 3234, 44 | 'extractPageNumber failed on relative URL'); 45 | 46 | assert.equal(serializer.extractPageNumber(null), null, 47 | 'extractPageNumber failed on null URL'); 48 | 49 | assert.equal(serializer.extractPageNumber('/a/p/'), null, 50 | 'extractPageNumber failed on URL without query params'); 51 | 52 | assert.equal(serializer.extractPageNumber('/a/p/?ordering=-timestamp&user=123'), null, 53 | 'extractPageNumber failed on URL with other query params'); 54 | 55 | assert.equal(serializer.extractPageNumber('/a/p/?fpage=23&pages=[1,2,3],page=123g&page=g123'), null, 56 | 'extractPageNumber failed on URL with similar query params'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dustinfarris/ember-django-adapter/2593628752fa924e1e073b2917c143c3e535af57/vendor/.gitkeep -------------------------------------------------------------------------------- /vendor/ember-django-adapter/register-version.js: -------------------------------------------------------------------------------- 1 | Ember.libraries.register('Ember Django Adapter', '2.1.1'); 2 | --------------------------------------------------------------------------------