├── server ├── __init__.py ├── test_settings.py ├── templates │ ├── include │ │ ├── jquery.html │ │ ├── ember.html │ │ ├── csrf.html │ │ └── head.html │ └── application.html ├── wsgi.py ├── urls.py └── settings.py ├── users ├── __init__.py ├── views.py ├── serializers.py ├── fixtures │ └── initial_data.json └── tests.py ├── session ├── __init__.py ├── views.py └── tests.py ├── circle.yml ├── assets ├── js │ ├── templates │ │ ├── application.handlebars │ │ └── session.handlebars │ └── app.js ├── tests │ ├── integration_test_helper.js │ └── integration_tests.js └── vendor │ ├── adapter.js │ ├── jquery.mockjax.js │ └── handlebars.js ├── package.json ├── requirements.txt ├── .gitignore ├── manage.py ├── Makefile ├── karma.conf.js └── README.md /server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /session/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/test_settings.py: -------------------------------------------------------------------------------- 1 | from server.settings import * 2 | 3 | 4 | DATABASES['default']['NAME'] = ':memory:' 5 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | override: 3 | - make develop 4 | 5 | test: 6 | override: 7 | - make test 8 | -------------------------------------------------------------------------------- /server/templates/include/jquery.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | -------------------------------------------------------------------------------- /assets/js/templates/application.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

Django/Ember Authentication

3 | {{render 'session'}} 4 | {{outlet}} 5 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "django-ember-precompile": "1.0.9", 4 | "karma-ember-preprocessor": "*", 5 | "karma-qunit": "*", 6 | "karma": "0.10.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e git+https://github.com/django/django.git@1.6b4#egg=django 2 | djangorestframework==2.3.8 3 | -e git+https://github.com/jezdez/django_compressor.git@9cf68a04e473b0bccf7e#egg=django_compressor 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.swp 3 | *.pyc 4 | *.pyo 5 | *.tar 6 | *.tgz 7 | *.tar.gz 8 | *.bak 9 | *.log 10 | .DS_Store 11 | .coverage 12 | env 13 | venv 14 | /media/ 15 | /static/ 16 | /node_modules/ 17 | *.sqlite3 18 | /vendor/ 19 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import viewsets 3 | 4 | from users import serializers 5 | 6 | 7 | class UserViewSet(viewsets.ModelViewSet): 8 | model = User 9 | serializer_class = serializers.UserSerializer 10 | -------------------------------------------------------------------------------- /users/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import serializers 3 | 4 | 5 | class UserSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = User 8 | fields = ('id', 'username', 'first_name', 'last_name') 9 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /server/templates/include/ember.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /server/templates/include/csrf.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "auth.user", 5 | "fields": { 6 | "username": "dustin", 7 | "first_name": "Dustin", 8 | "last_name": "Farris", 9 | "is_active": true, 10 | "password": "pbkdf2_sha256$10000$FxH0AVklG2MS$dmRkwyW/mFunUykRypX/X7K8k5CF8zSxrdxXPlwXqfQ=" 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /assets/tests/integration_test_helper.js: -------------------------------------------------------------------------------- 1 | document.write('
'); 2 | 3 | App.rootElement = '#ember-testing'; 4 | App.setupForTesting(); 5 | App.injectTestHelpers(); 6 | 7 | function exists(selector) { 8 | return !!find(selector).length; 9 | } 10 | 11 | function httpStub(url, json) { 12 | $.mockjax({ 13 | url: url, 14 | dataType: 'json', 15 | responseText: json 16 | }); 17 | } 18 | 19 | $.mockjaxSettings.logging = false; 20 | $.mockjaxSettings.responseTime = 0; -------------------------------------------------------------------------------- /server/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, patterns, url 2 | from django.views.generic import TemplateView 3 | from rest_framework.routers import DefaultRouter 4 | 5 | from session.views import SessionView 6 | from users.views import UserViewSet 7 | 8 | 9 | router = DefaultRouter() 10 | router.register(r'users', UserViewSet) 11 | 12 | 13 | urlpatterns = patterns( 14 | '', 15 | 16 | # Authentication 17 | url(r'^session/$', SessionView.as_view()), 18 | 19 | # API 20 | url(r'api/', include(router.urls)), 21 | 22 | # Application 23 | url(r'^$', TemplateView.as_view(template_name='application.html')) 24 | ) 25 | -------------------------------------------------------------------------------- /assets/js/templates/session.handlebars: -------------------------------------------------------------------------------- 1 | {{#if isAuthenticated}} 2 |

Welcome back, {{first_name}}!

3 | 4 | {{else}} 5 |
6 |
7 | {{input class="username" value=username type="text" placeholder="Username"}} 8 |
9 |
10 | {{input class="password" value=password type="password" placeholder="Password"}} 11 |
12 | {{input class="submit btn" type="submit" value="Login"}} 13 |
14 | {{#if errorMessage}}{{errorMessage}}{{/if}} 15 | {{/if}} -------------------------------------------------------------------------------- /server/templates/application.html: -------------------------------------------------------------------------------- 1 | {% load compress static %} 2 | 3 | {% include "head.html" %} 4 | 5 | 6 | 7 |
8 | 9 | 10 | {# Vendor javascripts #} 11 | {% include "jquery.html" %} 12 | {% include "csrf.html" %} 13 | {% include "ember.html" %} 14 | 15 | {# Application #} 16 | 17 | 18 | {# Handlebars templates #} 19 | {% compress js %} 20 | 21 | 22 | {% endcompress js %} 23 | 24 | -------------------------------------------------------------------------------- /server/templates/include/head.html: -------------------------------------------------------------------------------- 1 | {% load static compress %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}Django/Ember Authentication{% endblock title %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: lint test-python test-js 2 | 3 | lint: 4 | @echo "Linting Python files" 5 | flake8 --ignore=E121,W404,F403,E501 --exclude=./docs/*,./env/*,./venv/*,migrations,.git,./tests/functional/features,./tests/functional/steps . || exit 1 6 | @echo "" 7 | 8 | test-python: 9 | @echo "Running Python tests" 10 | python manage.py test --settings=server.test_settings || exit 1 11 | @echo "" 12 | 13 | test-js: 14 | @echo "Running QUnit Javascript tests" 15 | node_modules/.bin/karma start --reporters=dots || exit 1 16 | @echo "" 17 | 18 | develop: 19 | npm install 20 | pip install --upgrade setuptools 21 | pip install --upgrade "flake8>=2.0" 22 | pip install --upgrade -r requirements.txt 23 | python manage.py syncdb --noinput 24 | 25 | run: 26 | python manage.py runserver 0.0.0.0:8000 27 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(karma) { 2 | karma.set({ 3 | basePath: 'assets', 4 | 5 | files: [ 6 | "vendor/jquery.js", 7 | "vendor/handlebars.js", 8 | "vendor/ember.js", 9 | "vendor/ember-data.js", 10 | "vendor/adapter.js", 11 | "vendor/jquery.mockjax.js", 12 | "js/app.js", 13 | "tests/*.js", 14 | "js/templates/*.handlebars" 15 | ], 16 | 17 | logLevel: karma.LOG_ERROR, 18 | browsers: ['PhantomJS'], 19 | singleRun: true, 20 | autoWatch: false, 21 | 22 | frameworks: ["qunit"], 23 | 24 | plugins: [ 25 | 'karma-qunit', 26 | 'karma-chrome-launcher', 27 | 'karma-ember-preprocessor', 28 | 'karma-phantomjs-launcher' 29 | ], 30 | 31 | preprocessors: { 32 | "**/*.handlebars": 'ember' 33 | } 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /assets/tests/integration_tests.js: -------------------------------------------------------------------------------- 1 | module('integration tests', { 2 | setup: function() { 3 | Ember.run(function() { 4 | App.reset(); 5 | }); 6 | }, 7 | teardown: function() { 8 | $.mockjaxClear(); 9 | } 10 | }); 11 | 12 | test("user gives bad credentials and receives error message", function() { 13 | var login_response = {success: false, message: "bad user!", user_id: null} 14 | httpStub('/session/', login_response); 15 | 16 | visit("/").then(function() { 17 | fillIn(".username", "dustin"); 18 | fillIn(".password", "wrong"); 19 | return click(".submit"); 20 | }).then(function() { 21 | equal(find(".text-danger").text(), "bad user!", "error message was not detected") 22 | }) 23 | }); 24 | 25 | test("user logs in and receives welcome message", function() { 26 | var dustin = {id: 1, username: "dustin", first_name: "Dustin", last_name: "Farris"} 27 | var login_response = {success: true, user_id: 1} 28 | httpStub('/session/', login_response); 29 | httpStub('/api/users/1/', dustin); 30 | 31 | visit("/").then(function() { 32 | fillIn(".username", "dustin"); 33 | fillIn(".password", "right"); 34 | return click('.submit'); 35 | }).then(function() { 36 | equal(find("p.welcome").text(), "Welcome back, Dustin!", "welcome message was not detected"); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.urlresolvers import reverse 3 | from rest_framework import status 4 | from rest_framework.test import APITestCase 5 | 6 | from users.serializers import UserSerializer 7 | 8 | 9 | class CreateUserTest(APITestCase): 10 | def setUp(self): 11 | self.data = {'username': 'fred', 'first_name': 'Fred', 'last_name': 'Willums'} 12 | 13 | def test_can_create_user(self): 14 | response = self.client.post(reverse('user-list'), self.data) 15 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 16 | 17 | 18 | class ReadUserTest(APITestCase): 19 | def setUp(self): 20 | self.user = User.objects.create(username="jake") 21 | 22 | def test_can_read_user_list(self): 23 | response = self.client.get(reverse('user-list')) 24 | self.assertEqual(response.status_code, status.HTTP_200_OK) 25 | 26 | def test_can_read_user_detail(self): 27 | response = self.client.get(reverse('user-detail', args=[self.user.id])) 28 | self.assertEqual(response.status_code, status.HTTP_200_OK) 29 | 30 | 31 | class UpdateUserTest(APITestCase): 32 | def setUp(self): 33 | self.user = User.objects.create(username="jake", first_name="Jake") 34 | self.data = UserSerializer(self.user).data 35 | self.data.update({'first_name': 'Changed'}) 36 | 37 | def test_can_update_user(self): 38 | response = self.client.put(reverse('user-detail', args=[self.user.id]), self.data) 39 | self.assertEqual(response.status_code, status.HTTP_200_OK) 40 | 41 | 42 | class DeleteUserTest(APITestCase): 43 | def setUp(self): 44 | self.user = User.objects.create(username="jake") 45 | 46 | def test_can_delete_user(self): 47 | response = self.client.delete(reverse('user-detail', args=[self.user.id])) 48 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 49 | -------------------------------------------------------------------------------- /server/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 3 | TEMPLATE_DIRS = ( 4 | os.path.join(BASE_DIR, 'server', 'templates'), 5 | os.path.join(BASE_DIR, 'server', 'templates', 'include'), 6 | ) 7 | STATICFILES_DIRS = ( 8 | os.path.join(BASE_DIR, 'assets'), 9 | ) 10 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 11 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 12 | FIXTURE_DIRS = ( 13 | os.path.join(BASE_DIR, 'users', 'fixtures'), 14 | ) 15 | 16 | SECRET_KEY = 'v6)j@i0-she6pi-l*k2k395cu^*ic&8v1f0h=@q9p%pezu8607' 17 | DEBUG = True 18 | TEMPLATE_DEBUG = True 19 | ALLOWED_HOSTS = [] 20 | 21 | 22 | # Application definition 23 | 24 | INSTALLED_APPS = ( 25 | 'django.contrib.auth', 26 | 'django.contrib.contenttypes', 27 | 'django.contrib.sessions', 28 | 'django.contrib.staticfiles', 29 | 'compressor', 30 | ) 31 | 32 | MIDDLEWARE_CLASSES = ( 33 | 'django.contrib.sessions.middleware.SessionMiddleware', 34 | 'django.middleware.common.CommonMiddleware', 35 | 'django.middleware.csrf.CsrfViewMiddleware', 36 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 37 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 38 | ) 39 | 40 | ROOT_URLCONF = 'server.urls' 41 | 42 | WSGI_APPLICATION = 'server.wsgi.application' 43 | 44 | 45 | # Database 46 | 47 | DATABASES = { 48 | 'default': { 49 | 'ENGINE': 'django.db.backends.sqlite3', 50 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 51 | } 52 | } 53 | 54 | 55 | # Compressor 56 | 57 | NODE_ROOT = os.path.join(BASE_DIR, 'node_modules') 58 | HANDLEBARS_PATH = os.path.join(NODE_ROOT, 'django-ember-precompile', 'bin', 'django-ember-precompile') 59 | COMPRESS_PRECOMPILERS = ( 60 | ('text/x-handlebars', '{} {{infile}}'.format(HANDLEBARS_PATH)), 61 | ) 62 | 63 | 64 | # Static files (CSS, JavaScript, Images) 65 | 66 | STATIC_URL = '/static/' 67 | STATICFILES_FINDERS = ( 68 | 'django.contrib.staticfiles.finders.FileSystemFinder', 69 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 70 | 'compressor.finders.CompressorFinder', 71 | ) 72 | -------------------------------------------------------------------------------- /session/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate, login, logout 2 | from rest_framework import status 3 | from rest_framework.views import APIView 4 | from rest_framework.response import Response 5 | 6 | 7 | class SessionView(APIView): 8 | """ 9 | View to retrieve the current session if one exists, create a new 10 | session using a valid username and password, or destroy the session. 11 | """ 12 | error_messages = { 13 | 'invalid': "Invalid username or password", 14 | 'disabled': "Sorry, this account is suspended" 15 | } 16 | 17 | def _error_response(self, message_key): 18 | """ 19 | Return an error message. 20 | """ 21 | data = { 22 | 'success': False, 23 | 'message': self.error_messages[message_key], 24 | 'user_id': None, 25 | } 26 | return Response(data) 27 | 28 | def get(self, request, *args, **kwargs): 29 | """ 30 | Return the user id associated with this session if one exists. 31 | """ 32 | if request.user.is_authenticated(): 33 | return Response({'user_id': request.user.id}) 34 | return Response({'user_id': None}) 35 | 36 | def post(self, request, *args, **kwargs): 37 | """ 38 | Authenticate a user with a username and password. Return 39 | appropriate success flag, error messages, and user id. 40 | """ 41 | username = request.POST.get('username') 42 | password = request.POST.get('password') 43 | user = authenticate(username=username, password=password) 44 | if user is not None: 45 | if user.is_active: 46 | login(request, user) 47 | data = {'success': True, 'user_id': user.id} 48 | return Response(data) 49 | return self._error_response('disabled') 50 | return self._error_response('invalid') 51 | 52 | def delete(self, request, *args, **kwargs): 53 | """ 54 | Destroy the active session, effectively logging out the 55 | current user. 56 | """ 57 | logout(request) 58 | return Response(status=status.HTTP_204_NO_CONTENT) 59 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | App = Ember.Application.create(); 2 | 3 | 4 | // == Controllers == 5 | 6 | App.SessionController = Ember.ObjectController.extend({ 7 | username: null, 8 | password: null, 9 | errorMessage: null, 10 | 11 | reset: function() { 12 | this.setProperties({ 13 | username: null, 14 | password: null, 15 | errorMessage: null, 16 | model: null 17 | }); 18 | }, 19 | 20 | isAuthenticated: function() { 21 | return (!Ember.isEmpty(this.get('model'))); 22 | }.property('model'), 23 | 24 | setCurrentUser: function(user_id) { 25 | if (!Ember.isEmpty(user_id)) { 26 | var currentUser = this.store.find('user', user_id); 27 | this.set('model', currentUser); 28 | } 29 | }, 30 | 31 | actions: { 32 | login: function() { 33 | var self = this, data = this.getProperties('username', 'password'); 34 | $.post('/session/', data, null, 'json').then(function (response) { 35 | Ember.run(function() { 36 | self.set('errorMessage', response.message); 37 | self.setCurrentUser(response.user_id); 38 | }); 39 | }); 40 | }, 41 | logout: function() { 42 | $.ajax({url: '/session/', type: 'delete'}); 43 | this.reset(); 44 | this.transitionToRoute('index'); 45 | } 46 | } 47 | }); 48 | 49 | 50 | // == Models == 51 | 52 | var attr = DS.attr; 53 | 54 | App.User = DS.Model.extend({ 55 | username: attr(), 56 | first_name: attr(), 57 | last_name: attr(), 58 | }); 59 | 60 | 61 | // == Routes == 62 | 63 | App.Router.map(function() { 64 | this.resource("session"); 65 | }); 66 | 67 | App.ApplicationRoute = Ember.Route.extend({ 68 | setupController: function(controller, model) { 69 | var self = this; 70 | Ember.$.getJSON('/session/').then(function(response) { 71 | self.controllerFor('session').reset() 72 | self.controllerFor('session').setCurrentUser(response.user_id); 73 | }); 74 | } 75 | }); 76 | 77 | 78 | // == Adapter/Store == 79 | 80 | App.ApplicationAdapter = DS.DjangoRESTAdapter.extend({ 81 | namespace: 'api' 82 | }); 83 | -------------------------------------------------------------------------------- /session/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | 5 | 6 | class SessionViewTest(APITestCase): 7 | def setUp(self): 8 | self.user = User.objects.create( 9 | username="bob", 10 | first_name="Bob", 11 | last_name="Barker") 12 | self.user.set_password("correct") 13 | self.user.save() 14 | 15 | def tearDown(self): 16 | self.user.delete() 17 | 18 | def test_get_session_with_active_session(self): 19 | """ 20 | Should return a hash containing the current user's id. 21 | """ 22 | self.client.force_authenticate(user=self.user) 23 | response = self.client.get('/session/') 24 | expected = {'user_id': self.user.id} 25 | self.assertEqual(response.data, expected) 26 | 27 | def test_get_session_with_no_active_session(self): 28 | """ 29 | Should return a hash with `user_id` set to None. 30 | """ 31 | response = self.client.get('/session/') 32 | expected = {'user_id': None} 33 | self.assertEqual(response.data, expected) 34 | 35 | def test_create_session_with_valid_credentials(self): 36 | """ 37 | Should log the user in and return a JSON object with a success 38 | flag and the user's id. 39 | """ 40 | data = {'username': 'bob', 'password': 'correct'} 41 | response = self.client.post('/session/', data) 42 | expected = {'success': True, 'user_id': self.user.id} 43 | self.assertEqual(response.data, expected) 44 | self.assertEqual(self.client.session['_auth_user_id'], self.user.id) 45 | 46 | def test_create_session_with_invalid_credentials(self): 47 | """ 48 | Should not log the user in and return a JSON object with a 49 | failing success flag and error message. 50 | """ 51 | data = {'username': 'bob', 'password': 'wrong'} 52 | response = self.client.post('/session/', data) 53 | expected = {'success': False, 'user_id': None, 'message': 'Invalid username or password'} 54 | self.assertEqual(response.data, expected) 55 | self.assertNotIn('_auth_user_id', self.client.session) 56 | 57 | def test_create_session_when_user_is_not_active(self): 58 | """ 59 | Should not log the user in and return a JSON object with a 60 | failing success flag and error message. 61 | """ 62 | self.user.is_active = False 63 | self.user.save() 64 | data = {'username': 'bob', 'password': 'correct'} 65 | response = self.client.post('/session/', data) 66 | expected = {'success': False, 'user_id': None, 'message': 'Sorry, this account is suspended'} 67 | self.assertEqual(response.data, expected) 68 | self.assertNotIn('_auth_user_id', self.client.session) 69 | 70 | def test_destroy_session(self): 71 | """ 72 | Should log the user out and return an empty response. 73 | """ 74 | self.client.force_authenticate(user=self.user) 75 | response = self.client.delete('/session/') 76 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 77 | self.assertNotIn('_auth_user_id', self.client.session) 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django/Ember Authenticate 2 | 3 | ![CircleCI build status](https://circleci.com/gh/dustinfarris/django-ember-authentication.png) 4 | 5 | A simple how-to-do-it app that demonstrates one way to perform session-based authentication 6 | in [Ember](http://emberjs.com) using [Django](http://djangoproject.com/) as a backend. This 7 | application uses the latest builds of: 8 | 9 | * [Django][] 10 | * [Ember][] 11 | * [Ember Data][] 12 | * [Ember Data Django REST Adapter][] 13 | 14 | 15 | You should have [virtualenvwrapper][] installed. 16 | 17 | ### Installation 18 | 19 | ```console 20 | git clone git@github.com:dustinfarris/django-ember-authentication.git 21 | mkvirtualenv django-ember-authentication 22 | cd django-ember-authentication 23 | make develop 24 | ``` 25 | 26 | ### Run the tests 27 | 28 | ```console 29 | make 30 | ``` 31 | 32 | ### Fire up a temp server 33 | 34 | ```console 35 | make run 36 | ``` 37 | 38 | Navigate to http://localhost:8000 and log in with username **dustin** and password **correct**. 39 | 40 | ## Summary 41 | 42 | Authentication is probably one of the biggest initial hurdles when putting Ember to practical use. At 43 | least that has been my experience. Fortunately, with the right architecture, authenticating can be 44 | implemented with minimal code, and pain-free. 45 | 46 | I chose session-based authentication because it is built into Django and it relieves me of having to 47 | hack together token variables and remembering to include them on all my headers etc.. Most of the 48 | token-based solutions I've seen to date have not been pretty. Django's session backend does the 49 | majority of the work for you, really your only responsibility is the check the username and password 50 | (which Django has helpers for as well). 51 | 52 | ### Session 53 | 54 | A challenge I encountered every way I tried this was preserving the "current user" after successful 55 | authentication. Since the current user is really just a manifestation of the current session, I 56 | decided to call the resource "session" which, in my Ember implementation, has user properties. If 57 | this sounds confusing it will make sense when you look at the code. 58 | 59 | The session is its own resource, with its own route mapping and controller. On the server side, 60 | Django responds to three HTTP methods at the /session/ endpoint: GET, POST, and DELETE. GET gets the 61 | current session (if there is one), POST checks username/password credentials and then creates a 62 | session. DELETE logs the user out. Using regular HTTP methods this way makes it easy to integrate 63 | with both Ember and Django REST Framework. 64 | 65 | When Ember successfully authenticates, it queries the appropriate user and sets the session model to 66 | be that user. The user's properties then become instantly available to the session template, 67 | and to anything else with access to the SessionController. 68 | 69 | ## Acknowledgements 70 | 71 | Big thank you to the Ember team and [Tom Christie][] for Django REST Framework. These two projects 72 | have had a profound impact on modern web development and are very very exciting to work with. 73 | 74 | Also thanks to [Toran Billups][] for creating Ember Data Django REST Adapter without which combining 75 | Django and Ember would not be possible—or at least not as straight-forward. Also his [screencast] on 76 | integration testing Ember is an absolute must-watch. 77 | 78 | 79 | [Django]: https://github.com/django/django/releases/tag/1.6b4 80 | [Ember]: http://emberjs.com/builds/#/beta/latest 81 | [Ember Data]: http://emberjs.com/builds/#/canary/latest 82 | [Ember Data Django REST Adapter]: https://github.com/toranb/ember-data-django-rest-adapter/tree/ember1.0 83 | [virtualenvwrapper]: http://virtualenvwrapper.readthedocs.org/en/latest/ 84 | [Tom Christie]: https://github.com/tomchristie 85 | [Toran Billups]: https://github.com/toranb 86 | [screencast]: http://www.toranbillups.com/blog/archive/2013/07/21/Integration-testing-your-emberjs-app-with-QUnit-and-Karma/ 87 | -------------------------------------------------------------------------------- /assets/vendor/adapter.js: -------------------------------------------------------------------------------- 1 | // Version: 0.13.1-13-gd5fad5d 2 | // Last commit: d5fad5d (2013-09-15 20:29:23 -0500) 3 | 4 | 5 | (function() { 6 | var define, requireModule; 7 | 8 | (function() { 9 | var registry = {}, seen = {}; 10 | 11 | define = function(name, deps, callback) { 12 | registry[name] = { deps: deps, callback: callback }; 13 | }; 14 | 15 | requireModule = function(name) { 16 | if (seen[name]) { return seen[name]; } 17 | seen[name] = {}; 18 | 19 | var mod, deps, callback, reified , exports; 20 | 21 | mod = registry[name]; 22 | 23 | if (!mod) { 24 | throw new Error("Module '" + name + "' not found."); 25 | } 26 | 27 | deps = mod.deps; 28 | callback = mod.callback; 29 | reified = []; 30 | exports; 31 | 32 | for (var i=0, l=deps.length; i 1 && totalHydrated.length <= 1) { 160 | return this.buildUrlWithParentWhenAvailable(record, url, totalHydrated); 161 | } 162 | 163 | if (totalParents.length === 1 && totalHydrated.length === 1) { 164 | var parent_value = record.get(totalParents[0]).get('id'); //todo find pk (not always id) 165 | var parent_plural = Ember.String.pluralize(totalParents[0]); 166 | var endpoint = url.split('/').reverse()[1]; 167 | return url.replace(endpoint, parent_plural + "/" + parent_value + "/" + endpoint); 168 | } 169 | 170 | return url; 171 | }, 172 | 173 | buildUrlWithParentWhenAvailable: function(record, url, totalHydrated) { 174 | if (record && url && totalHydrated) { 175 | var parent_type = totalHydrated[0]; 176 | var parent_pk = record.get(parent_type).get('id'); //todo find pk (not always id) 177 | var parent_plural = Ember.String.pluralize(parent_type); 178 | var endpoint = url.split('/').reverse()[1]; 179 | url = url.replace(endpoint, parent_plural + "/" + parent_pk + "/" + endpoint); 180 | } 181 | return url; 182 | }, 183 | 184 | buildFindManyUrlWithParent: function(type, parent) { 185 | var root, url, endpoint, parentValue; 186 | 187 | endpoint = Ember.String.pluralize(type.typeKey); 188 | parentValue = parent.get('id'); //todo find pk (not always id) 189 | root = parent.constructor.typeKey; 190 | url = this.buildURL(root, parentValue); 191 | 192 | return url + endpoint + '/'; 193 | } 194 | 195 | }); 196 | 197 | })(); 198 | 199 | 200 | 201 | (function() { 202 | 203 | })(); 204 | 205 | 206 | })(); 207 | -------------------------------------------------------------------------------- /assets/vendor/jquery.mockjax.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * MockJax - jQuery Plugin to Mock Ajax requests 3 | * 4 | * Version: 1.5.2 5 | * Released: 6 | * Home: http://github.com/appendto/jquery-mockjax 7 | * Author: Jonathan Sharp (http://jdsharp.com) 8 | * License: MIT,GPL 9 | * 10 | * Copyright (c) 2011 appendTo LLC. 11 | * Dual licensed under the MIT or GPL licenses. 12 | * http://appendto.com/open-source-licenses 13 | */ 14 | (function($) { 15 | var _ajax = $.ajax, 16 | mockHandlers = [], 17 | mockedAjaxCalls = [], 18 | CALLBACK_REGEX = /=\?(&|$)/, 19 | jsc = (new Date()).getTime(); 20 | 21 | 22 | // Parse the given XML string. 23 | function parseXML(xml) { 24 | if ( window.DOMParser == undefined && window.ActiveXObject ) { 25 | DOMParser = function() { }; 26 | DOMParser.prototype.parseFromString = function( xmlString ) { 27 | var doc = new ActiveXObject('Microsoft.XMLDOM'); 28 | doc.async = 'false'; 29 | doc.loadXML( xmlString ); 30 | return doc; 31 | }; 32 | } 33 | 34 | try { 35 | var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' ); 36 | if ( $.isXMLDoc( xmlDoc ) ) { 37 | var err = $('parsererror', xmlDoc); 38 | if ( err.length == 1 ) { 39 | throw('Error: ' + $(xmlDoc).text() ); 40 | } 41 | } else { 42 | throw('Unable to parse XML'); 43 | } 44 | return xmlDoc; 45 | } catch( e ) { 46 | var msg = ( e.name == undefined ? e : e.name + ': ' + e.message ); 47 | $(document).trigger('xmlParseError', [ msg ]); 48 | return undefined; 49 | } 50 | } 51 | 52 | // Trigger a jQuery event 53 | function trigger(s, type, args) { 54 | (s.context ? $(s.context) : $.event).trigger(type, args); 55 | } 56 | 57 | // Check if the data field on the mock handler and the request match. This 58 | // can be used to restrict a mock handler to being used only when a certain 59 | // set of data is passed to it. 60 | function isMockDataEqual( mock, live ) { 61 | var identical = true; 62 | // Test for situations where the data is a querystring (not an object) 63 | if (typeof live === 'string') { 64 | // Querystring may be a regex 65 | return $.isFunction( mock.test ) ? mock.test(live) : mock == live; 66 | } 67 | $.each(mock, function(k) { 68 | if ( live[k] === undefined ) { 69 | identical = false; 70 | return identical; 71 | } else { 72 | if ( typeof live[k] == 'object' ) { 73 | identical = identical && isMockDataEqual(mock[k], live[k]); 74 | } else { 75 | if ( $.isFunction( mock[k].test ) ) { 76 | identical = identical && mock[k].test(live[k]); 77 | } else { 78 | identical = identical && ( mock[k] == live[k] ); 79 | } 80 | } 81 | } 82 | }); 83 | 84 | return identical; 85 | } 86 | 87 | // Check the given handler should mock the given request 88 | function getMockForRequest( handler, requestSettings ) { 89 | // If the mock was registered with a function, let the function decide if we 90 | // want to mock this request 91 | if ( $.isFunction(handler) ) { 92 | return handler( requestSettings ); 93 | } 94 | 95 | // Inspect the URL of the request and check if the mock handler's url 96 | // matches the url for this ajax request 97 | if ( $.isFunction(handler.url.test) ) { 98 | // The user provided a regex for the url, test it 99 | if ( !handler.url.test( requestSettings.url ) ) { 100 | return null; 101 | } 102 | } else { 103 | // Look for a simple wildcard '*' or a direct URL match 104 | var star = handler.url.indexOf('*'); 105 | if (handler.url !== requestSettings.url && star === -1 || 106 | !new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace(/\*/g, '.+')).test(requestSettings.url)) { 107 | return null; 108 | } 109 | } 110 | 111 | // Inspect the data submitted in the request (either POST body or GET query string) 112 | if ( handler.data && requestSettings.data ) { 113 | if ( !isMockDataEqual(handler.data, requestSettings.data) ) { 114 | // They're not identical, do not mock this request 115 | return null; 116 | } 117 | } 118 | // Inspect the request type 119 | if ( handler && handler.type && 120 | handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) { 121 | // The request type doesn't match (GET vs. POST) 122 | return null; 123 | } 124 | 125 | return handler; 126 | } 127 | 128 | // Process the xhr objects send operation 129 | function _xhrSend(mockHandler, requestSettings, origSettings) { 130 | 131 | // This is a substitute for < 1.4 which lacks $.proxy 132 | var process = (function(that) { 133 | return function() { 134 | return (function() { 135 | var onReady; 136 | 137 | // The request has returned 138 | this.status = mockHandler.status; 139 | this.statusText = mockHandler.statusText; 140 | this.readyState = 4; 141 | 142 | // We have an executable function, call it to give 143 | // the mock handler a chance to update it's data 144 | if ( $.isFunction(mockHandler.response) ) { 145 | mockHandler.response(origSettings); 146 | } 147 | // Copy over our mock to our xhr object before passing control back to 148 | // jQuery's onreadystatechange callback 149 | if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) { 150 | this.responseText = JSON.stringify(mockHandler.responseText); 151 | } else if ( requestSettings.dataType == 'xml' ) { 152 | if ( typeof mockHandler.responseXML == 'string' ) { 153 | this.responseXML = parseXML(mockHandler.responseXML); 154 | //in jQuery 1.9.1+, responseXML is processed differently and relies on responseText 155 | this.responseText = mockHandler.responseXML; 156 | } else { 157 | this.responseXML = mockHandler.responseXML; 158 | } 159 | } else { 160 | this.responseText = mockHandler.responseText; 161 | } 162 | if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) { 163 | this.status = mockHandler.status; 164 | } 165 | if( typeof mockHandler.statusText === "string") { 166 | this.statusText = mockHandler.statusText; 167 | } 168 | // jQuery 2.0 renamed onreadystatechange to onload 169 | onReady = this.onreadystatechange || this.onload; 170 | 171 | // jQuery < 1.4 doesn't have onreadystate change for xhr 172 | if ( $.isFunction( onReady ) ) { 173 | if( mockHandler.isTimeout) { 174 | this.status = -1; 175 | } 176 | onReady.call( this, mockHandler.isTimeout ? 'timeout' : undefined ); 177 | } else if ( mockHandler.isTimeout ) { 178 | // Fix for 1.3.2 timeout to keep success from firing. 179 | this.status = -1; 180 | } 181 | }).apply(that); 182 | }; 183 | })(this); 184 | 185 | if ( mockHandler.proxy ) { 186 | // We're proxying this request and loading in an external file instead 187 | _ajax({ 188 | global: false, 189 | url: mockHandler.proxy, 190 | type: mockHandler.proxyType, 191 | data: mockHandler.data, 192 | dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType, 193 | complete: function(xhr) { 194 | mockHandler.responseXML = xhr.responseXML; 195 | mockHandler.responseText = xhr.responseText; 196 | mockHandler.status = xhr.status; 197 | mockHandler.statusText = xhr.statusText; 198 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 0); 199 | } 200 | }); 201 | } else { 202 | // type == 'POST' || 'GET' || 'DELETE' 203 | if ( requestSettings.async === false ) { 204 | // TODO: Blocking delay 205 | process(); 206 | } else { 207 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 50); 208 | } 209 | } 210 | } 211 | 212 | // Construct a mocked XHR Object 213 | function xhr(mockHandler, requestSettings, origSettings, origHandler) { 214 | // Extend with our default mockjax settings 215 | mockHandler = $.extend(true, {}, $.mockjaxSettings, mockHandler); 216 | 217 | if (typeof mockHandler.headers === 'undefined') { 218 | mockHandler.headers = {}; 219 | } 220 | if ( mockHandler.contentType ) { 221 | mockHandler.headers['content-type'] = mockHandler.contentType; 222 | } 223 | 224 | return { 225 | status: mockHandler.status, 226 | statusText: mockHandler.statusText, 227 | readyState: 1, 228 | open: function() { }, 229 | send: function() { 230 | origHandler.fired = true; 231 | _xhrSend.call(this, mockHandler, requestSettings, origSettings); 232 | }, 233 | abort: function() { 234 | clearTimeout(this.responseTimer); 235 | }, 236 | setRequestHeader: function(header, value) { 237 | mockHandler.headers[header] = value; 238 | }, 239 | getResponseHeader: function(header) { 240 | // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery 241 | if ( mockHandler.headers && mockHandler.headers[header] ) { 242 | // Return arbitrary headers 243 | return mockHandler.headers[header]; 244 | } else if ( header.toLowerCase() == 'last-modified' ) { 245 | return mockHandler.lastModified || (new Date()).toString(); 246 | } else if ( header.toLowerCase() == 'etag' ) { 247 | return mockHandler.etag || ''; 248 | } else if ( header.toLowerCase() == 'content-type' ) { 249 | return mockHandler.contentType || 'text/plain'; 250 | } 251 | }, 252 | getAllResponseHeaders: function() { 253 | var headers = ''; 254 | $.each(mockHandler.headers, function(k, v) { 255 | headers += k + ': ' + v + "\n"; 256 | }); 257 | return headers; 258 | } 259 | }; 260 | } 261 | 262 | // Process a JSONP mock request. 263 | function processJsonpMock( requestSettings, mockHandler, origSettings ) { 264 | // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here 265 | // because there isn't an easy hook for the cross domain script tag of jsonp 266 | 267 | processJsonpUrl( requestSettings ); 268 | 269 | requestSettings.dataType = "json"; 270 | if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) { 271 | createJsonpCallback(requestSettings, mockHandler, origSettings); 272 | 273 | // We need to make sure 274 | // that a JSONP style response is executed properly 275 | 276 | var rurl = /^(\w+:)?\/\/([^\/?#]+)/, 277 | parts = rurl.exec( requestSettings.url ), 278 | remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); 279 | 280 | requestSettings.dataType = "script"; 281 | if(requestSettings.type.toUpperCase() === "GET" && remote ) { 282 | var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings ); 283 | 284 | // Check if we are supposed to return a Deferred back to the mock call, or just 285 | // signal success 286 | if(newMockReturn) { 287 | return newMockReturn; 288 | } else { 289 | return true; 290 | } 291 | } 292 | } 293 | return null; 294 | } 295 | 296 | // Append the required callback parameter to the end of the request URL, for a JSONP request 297 | function processJsonpUrl( requestSettings ) { 298 | if ( requestSettings.type.toUpperCase() === "GET" ) { 299 | if ( !CALLBACK_REGEX.test( requestSettings.url ) ) { 300 | requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") + 301 | (requestSettings.jsonp || "callback") + "=?"; 302 | } 303 | } else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) { 304 | requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?"; 305 | } 306 | } 307 | 308 | // Process a JSONP request by evaluating the mocked response text 309 | function processJsonpRequest( requestSettings, mockHandler, origSettings ) { 310 | // Synthesize the mock request for adding a script tag 311 | var callbackContext = origSettings && origSettings.context || requestSettings, 312 | newMock = null; 313 | 314 | 315 | // If the response handler on the moock is a function, call it 316 | if ( mockHandler.response && $.isFunction(mockHandler.response) ) { 317 | mockHandler.response(origSettings); 318 | } else { 319 | 320 | // Evaluate the responseText javascript in a global context 321 | if( typeof mockHandler.responseText === 'object' ) { 322 | $.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')'); 323 | } else { 324 | $.globalEval( '(' + mockHandler.responseText + ')'); 325 | } 326 | } 327 | 328 | // Successful response 329 | jsonpSuccess( requestSettings, callbackContext, mockHandler ); 330 | jsonpComplete( requestSettings, callbackContext, mockHandler ); 331 | 332 | // If we are running under jQuery 1.5+, return a deferred object 333 | if($.Deferred){ 334 | newMock = new $.Deferred(); 335 | if(typeof mockHandler.responseText == "object"){ 336 | newMock.resolveWith( callbackContext, [mockHandler.responseText] ); 337 | } 338 | else{ 339 | newMock.resolveWith( callbackContext, [$.parseJSON( mockHandler.responseText )] ); 340 | } 341 | } 342 | return newMock; 343 | } 344 | 345 | 346 | // Create the required JSONP callback function for the request 347 | function createJsonpCallback( requestSettings, mockHandler, origSettings ) { 348 | var callbackContext = origSettings && origSettings.context || requestSettings; 349 | var jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++); 350 | 351 | // Replace the =? sequence both in the query string and the data 352 | if ( requestSettings.data ) { 353 | requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 354 | } 355 | 356 | requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 357 | 358 | 359 | // Handle JSONP-style loading 360 | window[ jsonp ] = window[ jsonp ] || function( tmp ) { 361 | data = tmp; 362 | jsonpSuccess( requestSettings, callbackContext, mockHandler ); 363 | jsonpComplete( requestSettings, callbackContext, mockHandler ); 364 | // Garbage collect 365 | window[ jsonp ] = undefined; 366 | 367 | try { 368 | delete window[ jsonp ]; 369 | } catch(e) {} 370 | 371 | if ( head ) { 372 | head.removeChild( script ); 373 | } 374 | }; 375 | } 376 | 377 | // The JSONP request was successful 378 | function jsonpSuccess(requestSettings, callbackContext, mockHandler) { 379 | // If a local callback was specified, fire it and pass it the data 380 | if ( requestSettings.success ) { 381 | requestSettings.success.call( callbackContext, mockHandler.responseText || "", status, {} ); 382 | } 383 | 384 | // Fire the global callback 385 | if ( requestSettings.global ) { 386 | trigger(requestSettings, "ajaxSuccess", [{}, requestSettings] ); 387 | } 388 | } 389 | 390 | // The JSONP request was completed 391 | function jsonpComplete(requestSettings, callbackContext) { 392 | // Process result 393 | if ( requestSettings.complete ) { 394 | requestSettings.complete.call( callbackContext, {} , status ); 395 | } 396 | 397 | // The request was completed 398 | if ( requestSettings.global ) { 399 | trigger( "ajaxComplete", [{}, requestSettings] ); 400 | } 401 | 402 | // Handle the global AJAX counter 403 | if ( requestSettings.global && ! --$.active ) { 404 | $.event.trigger( "ajaxStop" ); 405 | } 406 | } 407 | 408 | 409 | // The core $.ajax replacement. 410 | function handleAjax( url, origSettings ) { 411 | var mockRequest, requestSettings, mockHandler; 412 | 413 | // If url is an object, simulate pre-1.5 signature 414 | if ( typeof url === "object" ) { 415 | origSettings = url; 416 | url = undefined; 417 | } else { 418 | // work around to support 1.5 signature 419 | origSettings.url = url; 420 | } 421 | 422 | // Extend the original settings for the request 423 | requestSettings = $.extend(true, {}, $.ajaxSettings, origSettings); 424 | 425 | // Iterate over our mock handlers (in registration order) until we find 426 | // one that is willing to intercept the request 427 | for(var k = 0; k < mockHandlers.length; k++) { 428 | if ( !mockHandlers[k] ) { 429 | continue; 430 | } 431 | 432 | mockHandler = getMockForRequest( mockHandlers[k], requestSettings ); 433 | if(!mockHandler) { 434 | // No valid mock found for this request 435 | continue; 436 | } 437 | 438 | mockedAjaxCalls.push(requestSettings); 439 | 440 | // If logging is enabled, log the mock to the console 441 | $.mockjaxSettings.log( mockHandler, requestSettings ); 442 | 443 | 444 | if ( requestSettings.dataType === "jsonp" ) { 445 | if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) { 446 | // This mock will handle the JSONP request 447 | return mockRequest; 448 | } 449 | } 450 | 451 | 452 | // Removed to fix #54 - keep the mocking data object intact 453 | //mockHandler.data = requestSettings.data; 454 | 455 | mockHandler.cache = requestSettings.cache; 456 | mockHandler.timeout = requestSettings.timeout; 457 | mockHandler.global = requestSettings.global; 458 | 459 | copyUrlParameters(mockHandler, origSettings); 460 | 461 | (function(mockHandler, requestSettings, origSettings, origHandler) { 462 | mockRequest = _ajax.call($, $.extend(true, {}, origSettings, { 463 | // Mock the XHR object 464 | xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ); } 465 | })); 466 | })(mockHandler, requestSettings, origSettings, mockHandlers[k]); 467 | 468 | return mockRequest; 469 | } 470 | 471 | // We don't have a mock request, trigger a normal request 472 | return _ajax.apply($, [origSettings]); 473 | } 474 | 475 | /** 476 | * Copies URL parameter values if they were captured by a regular expression 477 | * @param {Object} mockHandler 478 | * @param {Object} origSettings 479 | */ 480 | function copyUrlParameters(mockHandler, origSettings) { 481 | //parameters aren't captured if the URL isn't a RegExp 482 | if (!(mockHandler.url instanceof RegExp)) { 483 | return; 484 | } 485 | //if no URL params were defined on the handler, don't attempt a capture 486 | if (!mockHandler.hasOwnProperty('urlParams')) { 487 | return; 488 | } 489 | var captures = mockHandler.url.exec(origSettings.url); 490 | //the whole RegExp match is always the first value in the capture results 491 | if (captures.length === 1) { 492 | return; 493 | } 494 | captures.shift(); 495 | //use handler params as keys and capture resuts as values 496 | var i = 0, 497 | capturesLength = captures.length, 498 | paramsLength = mockHandler.urlParams.length, 499 | //in case the number of params specified is less than actual captures 500 | maxIterations = Math.min(capturesLength, paramsLength), 501 | paramValues = {}; 502 | for (i; i < maxIterations; i++) { 503 | var key = mockHandler.urlParams[i]; 504 | paramValues[key] = captures[i]; 505 | } 506 | origSettings.urlParams = paramValues; 507 | } 508 | 509 | 510 | // Public 511 | 512 | $.extend({ 513 | ajax: handleAjax 514 | }); 515 | 516 | $.mockjaxSettings = { 517 | //url: null, 518 | //type: 'GET', 519 | log: function( mockHandler, requestSettings ) { 520 | if ( mockHandler.logging === false || 521 | ( typeof mockHandler.logging === 'undefined' && $.mockjaxSettings.logging === false ) ) { 522 | return; 523 | } 524 | if ( window.console && console.log ) { 525 | var message = 'MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url; 526 | var request = $.extend({}, requestSettings); 527 | 528 | if (typeof console.log === 'function') { 529 | console.log(message, request); 530 | } else { 531 | try { 532 | console.log( message + ' ' + JSON.stringify(request) ); 533 | } catch (e) { 534 | console.log(message); 535 | } 536 | } 537 | } 538 | }, 539 | logging: true, 540 | status: 200, 541 | statusText: "OK", 542 | responseTime: 500, 543 | isTimeout: false, 544 | contentType: 'text/plain', 545 | response: '', 546 | responseText: '', 547 | responseXML: '', 548 | proxy: '', 549 | proxyType: 'GET', 550 | 551 | lastModified: null, 552 | etag: '', 553 | headers: { 554 | etag: 'IJF@H#@923uf8023hFO@I#H#', 555 | 'content-type' : 'text/plain' 556 | } 557 | }; 558 | 559 | $.mockjax = function(settings) { 560 | var i = mockHandlers.length; 561 | mockHandlers[i] = settings; 562 | return i; 563 | }; 564 | $.mockjaxClear = function(i) { 565 | if ( arguments.length == 1 ) { 566 | mockHandlers[i] = null; 567 | } else { 568 | mockHandlers = []; 569 | } 570 | mockedAjaxCalls = []; 571 | }; 572 | $.mockjax.handler = function(i) { 573 | if ( arguments.length == 1 ) { 574 | return mockHandlers[i]; 575 | } 576 | }; 577 | $.mockjax.mockedAjaxCalls = function() { 578 | return mockedAjaxCalls; 579 | }; 580 | })(jQuery); 581 | -------------------------------------------------------------------------------- /assets/vendor/handlebars.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (C) 2011 by Yehuda Katz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | // lib/handlebars/browser-prefix.js 26 | var Handlebars = {}; 27 | 28 | (function(Handlebars, undefined) { 29 | ; 30 | // lib/handlebars/base.js 31 | 32 | Handlebars.VERSION = "1.0.0"; 33 | Handlebars.COMPILER_REVISION = 4; 34 | 35 | Handlebars.REVISION_CHANGES = { 36 | 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 37 | 2: '== 1.0.0-rc.3', 38 | 3: '== 1.0.0-rc.4', 39 | 4: '>= 1.0.0' 40 | }; 41 | 42 | Handlebars.helpers = {}; 43 | Handlebars.partials = {}; 44 | 45 | var toString = Object.prototype.toString, 46 | functionType = '[object Function]', 47 | objectType = '[object Object]'; 48 | 49 | Handlebars.registerHelper = function(name, fn, inverse) { 50 | if (toString.call(name) === objectType) { 51 | if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); } 52 | Handlebars.Utils.extend(this.helpers, name); 53 | } else { 54 | if (inverse) { fn.not = inverse; } 55 | this.helpers[name] = fn; 56 | } 57 | }; 58 | 59 | Handlebars.registerPartial = function(name, str) { 60 | if (toString.call(name) === objectType) { 61 | Handlebars.Utils.extend(this.partials, name); 62 | } else { 63 | this.partials[name] = str; 64 | } 65 | }; 66 | 67 | Handlebars.registerHelper('helperMissing', function(arg) { 68 | if(arguments.length === 2) { 69 | return undefined; 70 | } else { 71 | throw new Error("Missing helper: '" + arg + "'"); 72 | } 73 | }); 74 | 75 | Handlebars.registerHelper('blockHelperMissing', function(context, options) { 76 | var inverse = options.inverse || function() {}, fn = options.fn; 77 | 78 | var type = toString.call(context); 79 | 80 | if(type === functionType) { context = context.call(this); } 81 | 82 | if(context === true) { 83 | return fn(this); 84 | } else if(context === false || context == null) { 85 | return inverse(this); 86 | } else if(type === "[object Array]") { 87 | if(context.length > 0) { 88 | return Handlebars.helpers.each(context, options); 89 | } else { 90 | return inverse(this); 91 | } 92 | } else { 93 | return fn(context); 94 | } 95 | }); 96 | 97 | Handlebars.K = function() {}; 98 | 99 | Handlebars.createFrame = Object.create || function(object) { 100 | Handlebars.K.prototype = object; 101 | var obj = new Handlebars.K(); 102 | Handlebars.K.prototype = null; 103 | return obj; 104 | }; 105 | 106 | Handlebars.logger = { 107 | DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, 108 | 109 | methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'}, 110 | 111 | // can be overridden in the host environment 112 | log: function(level, obj) { 113 | if (Handlebars.logger.level <= level) { 114 | var method = Handlebars.logger.methodMap[level]; 115 | if (typeof console !== 'undefined' && console[method]) { 116 | console[method].call(console, obj); 117 | } 118 | } 119 | } 120 | }; 121 | 122 | Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); }; 123 | 124 | Handlebars.registerHelper('each', function(context, options) { 125 | var fn = options.fn, inverse = options.inverse; 126 | var i = 0, ret = "", data; 127 | 128 | var type = toString.call(context); 129 | if(type === functionType) { context = context.call(this); } 130 | 131 | if (options.data) { 132 | data = Handlebars.createFrame(options.data); 133 | } 134 | 135 | if(context && typeof context === 'object') { 136 | if(context instanceof Array){ 137 | for(var j = context.length; i 2) { 351 | expected.push("'" + this.terminals_[p] + "'"); 352 | } 353 | if (this.lexer.showPosition) { 354 | errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; 355 | } else { 356 | errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); 357 | } 358 | this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 359 | } 360 | } 361 | if (action[0] instanceof Array && action.length > 1) { 362 | throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); 363 | } 364 | switch (action[0]) { 365 | case 1: 366 | stack.push(symbol); 367 | vstack.push(this.lexer.yytext); 368 | lstack.push(this.lexer.yylloc); 369 | stack.push(action[1]); 370 | symbol = null; 371 | if (!preErrorSymbol) { 372 | yyleng = this.lexer.yyleng; 373 | yytext = this.lexer.yytext; 374 | yylineno = this.lexer.yylineno; 375 | yyloc = this.lexer.yylloc; 376 | if (recovering > 0) 377 | recovering--; 378 | } else { 379 | symbol = preErrorSymbol; 380 | preErrorSymbol = null; 381 | } 382 | break; 383 | case 2: 384 | len = this.productions_[action[1]][1]; 385 | yyval.$ = vstack[vstack.length - len]; 386 | yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; 387 | if (ranges) { 388 | yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; 389 | } 390 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 391 | if (typeof r !== "undefined") { 392 | return r; 393 | } 394 | if (len) { 395 | stack = stack.slice(0, -1 * len * 2); 396 | vstack = vstack.slice(0, -1 * len); 397 | lstack = lstack.slice(0, -1 * len); 398 | } 399 | stack.push(this.productions_[action[1]][0]); 400 | vstack.push(yyval.$); 401 | lstack.push(yyval._$); 402 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 403 | stack.push(newState); 404 | break; 405 | case 3: 406 | return true; 407 | } 408 | } 409 | return true; 410 | } 411 | }; 412 | /* Jison generated lexer */ 413 | var lexer = (function(){ 414 | var lexer = ({EOF:1, 415 | parseError:function parseError(str, hash) { 416 | if (this.yy.parser) { 417 | this.yy.parser.parseError(str, hash); 418 | } else { 419 | throw new Error(str); 420 | } 421 | }, 422 | setInput:function (input) { 423 | this._input = input; 424 | this._more = this._less = this.done = false; 425 | this.yylineno = this.yyleng = 0; 426 | this.yytext = this.matched = this.match = ''; 427 | this.conditionStack = ['INITIAL']; 428 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 429 | if (this.options.ranges) this.yylloc.range = [0,0]; 430 | this.offset = 0; 431 | return this; 432 | }, 433 | input:function () { 434 | var ch = this._input[0]; 435 | this.yytext += ch; 436 | this.yyleng++; 437 | this.offset++; 438 | this.match += ch; 439 | this.matched += ch; 440 | var lines = ch.match(/(?:\r\n?|\n).*/g); 441 | if (lines) { 442 | this.yylineno++; 443 | this.yylloc.last_line++; 444 | } else { 445 | this.yylloc.last_column++; 446 | } 447 | if (this.options.ranges) this.yylloc.range[1]++; 448 | 449 | this._input = this._input.slice(1); 450 | return ch; 451 | }, 452 | unput:function (ch) { 453 | var len = ch.length; 454 | var lines = ch.split(/(?:\r\n?|\n)/g); 455 | 456 | this._input = ch + this._input; 457 | this.yytext = this.yytext.substr(0, this.yytext.length-len-1); 458 | //this.yyleng -= len; 459 | this.offset -= len; 460 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 461 | this.match = this.match.substr(0, this.match.length-1); 462 | this.matched = this.matched.substr(0, this.matched.length-1); 463 | 464 | if (lines.length-1) this.yylineno -= lines.length-1; 465 | var r = this.yylloc.range; 466 | 467 | this.yylloc = {first_line: this.yylloc.first_line, 468 | last_line: this.yylineno+1, 469 | first_column: this.yylloc.first_column, 470 | last_column: lines ? 471 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: 472 | this.yylloc.first_column - len 473 | }; 474 | 475 | if (this.options.ranges) { 476 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 477 | } 478 | return this; 479 | }, 480 | more:function () { 481 | this._more = true; 482 | return this; 483 | }, 484 | less:function (n) { 485 | this.unput(this.match.slice(n)); 486 | }, 487 | pastInput:function () { 488 | var past = this.matched.substr(0, this.matched.length - this.match.length); 489 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 490 | }, 491 | upcomingInput:function () { 492 | var next = this.match; 493 | if (next.length < 20) { 494 | next += this._input.substr(0, 20-next.length); 495 | } 496 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 497 | }, 498 | showPosition:function () { 499 | var pre = this.pastInput(); 500 | var c = new Array(pre.length + 1).join("-"); 501 | return pre + this.upcomingInput() + "\n" + c+"^"; 502 | }, 503 | next:function () { 504 | if (this.done) { 505 | return this.EOF; 506 | } 507 | if (!this._input) this.done = true; 508 | 509 | var token, 510 | match, 511 | tempMatch, 512 | index, 513 | col, 514 | lines; 515 | if (!this._more) { 516 | this.yytext = ''; 517 | this.match = ''; 518 | } 519 | var rules = this._currentRules(); 520 | for (var i=0;i < rules.length; i++) { 521 | tempMatch = this._input.match(this.rules[rules[i]]); 522 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 523 | match = tempMatch; 524 | index = i; 525 | if (!this.options.flex) break; 526 | } 527 | } 528 | if (match) { 529 | lines = match[0].match(/(?:\r\n?|\n).*/g); 530 | if (lines) this.yylineno += lines.length; 531 | this.yylloc = {first_line: this.yylloc.last_line, 532 | last_line: this.yylineno+1, 533 | first_column: this.yylloc.last_column, 534 | last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; 535 | this.yytext += match[0]; 536 | this.match += match[0]; 537 | this.matches = match; 538 | this.yyleng = this.yytext.length; 539 | if (this.options.ranges) { 540 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 541 | } 542 | this._more = false; 543 | this._input = this._input.slice(match[0].length); 544 | this.matched += match[0]; 545 | token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); 546 | if (this.done && this._input) this.done = false; 547 | if (token) return token; 548 | else return; 549 | } 550 | if (this._input === "") { 551 | return this.EOF; 552 | } else { 553 | return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 554 | {text: "", token: null, line: this.yylineno}); 555 | } 556 | }, 557 | lex:function lex() { 558 | var r = this.next(); 559 | if (typeof r !== 'undefined') { 560 | return r; 561 | } else { 562 | return this.lex(); 563 | } 564 | }, 565 | begin:function begin(condition) { 566 | this.conditionStack.push(condition); 567 | }, 568 | popState:function popState() { 569 | return this.conditionStack.pop(); 570 | }, 571 | _currentRules:function _currentRules() { 572 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 573 | }, 574 | topState:function () { 575 | return this.conditionStack[this.conditionStack.length-2]; 576 | }, 577 | pushState:function begin(condition) { 578 | this.begin(condition); 579 | }}); 580 | lexer.options = {}; 581 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 582 | 583 | var YYSTATE=YY_START 584 | switch($avoiding_name_collisions) { 585 | case 0: yy_.yytext = "\\"; return 14; 586 | break; 587 | case 1: 588 | if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); 589 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); 590 | if(yy_.yytext) return 14; 591 | 592 | break; 593 | case 2: return 14; 594 | break; 595 | case 3: 596 | if(yy_.yytext.slice(-1) !== "\\") this.popState(); 597 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); 598 | return 14; 599 | 600 | break; 601 | case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; 602 | break; 603 | case 5: return 25; 604 | break; 605 | case 6: return 16; 606 | break; 607 | case 7: return 20; 608 | break; 609 | case 8: return 19; 610 | break; 611 | case 9: return 19; 612 | break; 613 | case 10: return 23; 614 | break; 615 | case 11: return 22; 616 | break; 617 | case 12: this.popState(); this.begin('com'); 618 | break; 619 | case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 620 | break; 621 | case 14: return 22; 622 | break; 623 | case 15: return 37; 624 | break; 625 | case 16: return 36; 626 | break; 627 | case 17: return 36; 628 | break; 629 | case 18: return 40; 630 | break; 631 | case 19: /*ignore whitespace*/ 632 | break; 633 | case 20: this.popState(); return 24; 634 | break; 635 | case 21: this.popState(); return 18; 636 | break; 637 | case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 31; 638 | break; 639 | case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 31; 640 | break; 641 | case 24: return 38; 642 | break; 643 | case 25: return 33; 644 | break; 645 | case 26: return 33; 646 | break; 647 | case 27: return 32; 648 | break; 649 | case 28: return 36; 650 | break; 651 | case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 36; 652 | break; 653 | case 30: return 'INVALID'; 654 | break; 655 | case 31: return 5; 656 | break; 657 | } 658 | }; 659 | lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; 660 | lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}}; 661 | return lexer;})() 662 | parser.lexer = lexer; 663 | function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; 664 | return new Parser; 665 | })();; 666 | // lib/handlebars/compiler/base.js 667 | 668 | Handlebars.Parser = handlebars; 669 | 670 | Handlebars.parse = function(input) { 671 | 672 | // Just return if an already-compile AST was passed in. 673 | if(input.constructor === Handlebars.AST.ProgramNode) { return input; } 674 | 675 | Handlebars.Parser.yy = Handlebars.AST; 676 | return Handlebars.Parser.parse(input); 677 | }; 678 | ; 679 | // lib/handlebars/compiler/ast.js 680 | Handlebars.AST = {}; 681 | 682 | Handlebars.AST.ProgramNode = function(statements, inverse) { 683 | this.type = "program"; 684 | this.statements = statements; 685 | if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } 686 | }; 687 | 688 | Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { 689 | this.type = "mustache"; 690 | this.escaped = !unescaped; 691 | this.hash = hash; 692 | 693 | var id = this.id = rawParams[0]; 694 | var params = this.params = rawParams.slice(1); 695 | 696 | // a mustache is an eligible helper if: 697 | // * its id is simple (a single part, not `this` or `..`) 698 | var eligibleHelper = this.eligibleHelper = id.isSimple; 699 | 700 | // a mustache is definitely a helper if: 701 | // * it is an eligible helper, and 702 | // * it has at least one parameter or hash segment 703 | this.isHelper = eligibleHelper && (params.length || hash); 704 | 705 | // if a mustache is an eligible helper but not a definite 706 | // helper, it is ambiguous, and will be resolved in a later 707 | // pass or at runtime. 708 | }; 709 | 710 | Handlebars.AST.PartialNode = function(partialName, context) { 711 | this.type = "partial"; 712 | this.partialName = partialName; 713 | this.context = context; 714 | }; 715 | 716 | Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { 717 | var verifyMatch = function(open, close) { 718 | if(open.original !== close.original) { 719 | throw new Handlebars.Exception(open.original + " doesn't match " + close.original); 720 | } 721 | }; 722 | 723 | verifyMatch(mustache.id, close); 724 | this.type = "block"; 725 | this.mustache = mustache; 726 | this.program = program; 727 | this.inverse = inverse; 728 | 729 | if (this.inverse && !this.program) { 730 | this.isInverse = true; 731 | } 732 | }; 733 | 734 | Handlebars.AST.ContentNode = function(string) { 735 | this.type = "content"; 736 | this.string = string; 737 | }; 738 | 739 | Handlebars.AST.HashNode = function(pairs) { 740 | this.type = "hash"; 741 | this.pairs = pairs; 742 | }; 743 | 744 | Handlebars.AST.IdNode = function(parts) { 745 | this.type = "ID"; 746 | 747 | var original = "", 748 | dig = [], 749 | depth = 0; 750 | 751 | for(var i=0,l=parts.length; i 0) { throw new Handlebars.Exception("Invalid path: " + original); } 757 | else if (part === "..") { depth++; } 758 | else { this.isScoped = true; } 759 | } 760 | else { dig.push(part); } 761 | } 762 | 763 | this.original = original; 764 | this.parts = dig; 765 | this.string = dig.join('.'); 766 | this.depth = depth; 767 | 768 | // an ID is simple if it only has one part, and that part is not 769 | // `..` or `this`. 770 | this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; 771 | 772 | this.stringModeValue = this.string; 773 | }; 774 | 775 | Handlebars.AST.PartialNameNode = function(name) { 776 | this.type = "PARTIAL_NAME"; 777 | this.name = name.original; 778 | }; 779 | 780 | Handlebars.AST.DataNode = function(id) { 781 | this.type = "DATA"; 782 | this.id = id; 783 | }; 784 | 785 | Handlebars.AST.StringNode = function(string) { 786 | this.type = "STRING"; 787 | this.original = 788 | this.string = 789 | this.stringModeValue = string; 790 | }; 791 | 792 | Handlebars.AST.IntegerNode = function(integer) { 793 | this.type = "INTEGER"; 794 | this.original = 795 | this.integer = integer; 796 | this.stringModeValue = Number(integer); 797 | }; 798 | 799 | Handlebars.AST.BooleanNode = function(bool) { 800 | this.type = "BOOLEAN"; 801 | this.bool = bool; 802 | this.stringModeValue = bool === "true"; 803 | }; 804 | 805 | Handlebars.AST.CommentNode = function(comment) { 806 | this.type = "comment"; 807 | this.comment = comment; 808 | }; 809 | ; 810 | // lib/handlebars/utils.js 811 | 812 | var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; 813 | 814 | Handlebars.Exception = function(message) { 815 | var tmp = Error.prototype.constructor.apply(this, arguments); 816 | 817 | // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. 818 | for (var idx = 0; idx < errorProps.length; idx++) { 819 | this[errorProps[idx]] = tmp[errorProps[idx]]; 820 | } 821 | }; 822 | Handlebars.Exception.prototype = new Error(); 823 | 824 | // Build out our basic SafeString type 825 | Handlebars.SafeString = function(string) { 826 | this.string = string; 827 | }; 828 | Handlebars.SafeString.prototype.toString = function() { 829 | return this.string.toString(); 830 | }; 831 | 832 | var escape = { 833 | "&": "&", 834 | "<": "<", 835 | ">": ">", 836 | '"': """, 837 | "'": "'", 838 | "`": "`" 839 | }; 840 | 841 | var badChars = /[&<>"'`]/g; 842 | var possible = /[&<>"'`]/; 843 | 844 | var escapeChar = function(chr) { 845 | return escape[chr] || "&"; 846 | }; 847 | 848 | Handlebars.Utils = { 849 | extend: function(obj, value) { 850 | for(var key in value) { 851 | if(value.hasOwnProperty(key)) { 852 | obj[key] = value[key]; 853 | } 854 | } 855 | }, 856 | 857 | escapeExpression: function(string) { 858 | // don't escape SafeStrings, since they're already safe 859 | if (string instanceof Handlebars.SafeString) { 860 | return string.toString(); 861 | } else if (string == null || string === false) { 862 | return ""; 863 | } 864 | 865 | // Force a string conversion as this will be done by the append regardless and 866 | // the regex test will do this transparently behind the scenes, causing issues if 867 | // an object's to string has escaped characters in it. 868 | string = string.toString(); 869 | 870 | if(!possible.test(string)) { return string; } 871 | return string.replace(badChars, escapeChar); 872 | }, 873 | 874 | isEmpty: function(value) { 875 | if (!value && value !== 0) { 876 | return true; 877 | } else if(toString.call(value) === "[object Array]" && value.length === 0) { 878 | return true; 879 | } else { 880 | return false; 881 | } 882 | } 883 | }; 884 | ; 885 | // lib/handlebars/compiler/compiler.js 886 | 887 | /*jshint eqnull:true*/ 888 | var Compiler = Handlebars.Compiler = function() {}; 889 | var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function() {}; 890 | 891 | // the foundHelper register will disambiguate helper lookup from finding a 892 | // function in a context. This is necessary for mustache compatibility, which 893 | // requires that context functions in blocks are evaluated by blockHelperMissing, 894 | // and then proceed as if the resulting value was provided to blockHelperMissing. 895 | 896 | Compiler.prototype = { 897 | compiler: Compiler, 898 | 899 | disassemble: function() { 900 | var opcodes = this.opcodes, opcode, out = [], params, param; 901 | 902 | for (var i=0, l=opcodes.length; i 0) { 1414 | this.source[1] = this.source[1] + ", " + locals.join(", "); 1415 | } 1416 | 1417 | // Generate minimizer alias mappings 1418 | if (!this.isChild) { 1419 | for (var alias in this.context.aliases) { 1420 | if (this.context.aliases.hasOwnProperty(alias)) { 1421 | this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; 1422 | } 1423 | } 1424 | } 1425 | 1426 | if (this.source[1]) { 1427 | this.source[1] = "var " + this.source[1].substring(2) + ";"; 1428 | } 1429 | 1430 | // Merge children 1431 | if (!this.isChild) { 1432 | this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; 1433 | } 1434 | 1435 | if (!this.environment.isSimple) { 1436 | this.source.push("return buffer;"); 1437 | } 1438 | 1439 | var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; 1440 | 1441 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1975 | return this.topStackName(); 1976 | }, 1977 | topStackName: function() { 1978 | return "stack" + this.stackSlot; 1979 | }, 1980 | flushInline: function() { 1981 | var inlineStack = this.inlineStack; 1982 | if (inlineStack.length) { 1983 | this.inlineStack = []; 1984 | for (var i = 0, len = inlineStack.length; i < len; i++) { 1985 | var entry = inlineStack[i]; 1986 | if (entry instanceof Literal) { 1987 | this.compileStack.push(entry); 1988 | } else { 1989 | this.pushStack(entry); 1990 | } 1991 | } 1992 | } 1993 | }, 1994 | isInline: function() { 1995 | return this.inlineStack.length; 1996 | }, 1997 | 1998 | popStack: function(wrapped) { 1999 | var inline = this.isInline(), 2000 | item = (inline ? this.inlineStack : this.compileStack).pop(); 2001 | 2002 | if (!wrapped && (item instanceof Literal)) { 2003 | return item.value; 2004 | } else { 2005 | if (!inline) { 2006 | this.stackSlot--; 2007 | } 2008 | return item; 2009 | } 2010 | }, 2011 | 2012 | topStack: function(wrapped) { 2013 | var stack = (this.isInline() ? this.inlineStack : this.compileStack), 2014 | item = stack[stack.length - 1]; 2015 | 2016 | if (!wrapped && (item instanceof Literal)) { 2017 | return item.value; 2018 | } else { 2019 | return item; 2020 | } 2021 | }, 2022 | 2023 | quotedString: function(str) { 2024 | return '"' + str 2025 | .replace(/\\/g, '\\\\') 2026 | .replace(/"/g, '\\"') 2027 | .replace(/\n/g, '\\n') 2028 | .replace(/\r/g, '\\r') 2029 | .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 2030 | .replace(/\u2029/g, '\\u2029') + '"'; 2031 | }, 2032 | 2033 | setupHelper: function(paramSize, name, missingParams) { 2034 | var params = []; 2035 | this.setupParams(paramSize, params, missingParams); 2036 | var foundHelper = this.nameLookup('helpers', name, 'helper'); 2037 | 2038 | return { 2039 | params: params, 2040 | name: foundHelper, 2041 | callParams: ["depth0"].concat(params).join(", "), 2042 | helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") 2043 | }; 2044 | }, 2045 | 2046 | // the params and contexts arguments are passed in arrays 2047 | // to fill in 2048 | setupParams: function(paramSize, params, useRegister) { 2049 | var options = [], contexts = [], types = [], param, inverse, program; 2050 | 2051 | options.push("hash:" + this.popStack()); 2052 | 2053 | inverse = this.popStack(); 2054 | program = this.popStack(); 2055 | 2056 | // Avoid setting fn and inverse if neither are set. This allows 2057 | // helpers to do a check for `if (options.fn)` 2058 | if (program || inverse) { 2059 | if (!program) { 2060 | this.context.aliases.self = "this"; 2061 | program = "self.noop"; 2062 | } 2063 | 2064 | if (!inverse) { 2065 | this.context.aliases.self = "this"; 2066 | inverse = "self.noop"; 2067 | } 2068 | 2069 | options.push("inverse:" + inverse); 2070 | options.push("fn:" + program); 2071 | } 2072 | 2073 | for(var i=0; i