├── .coveragerc ├── .dockerignore ├── .gitattributes ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── authors-info.yml ├── client ├── README.md ├── karma.conf.js ├── package.json ├── src │ └── js │ │ ├── djng-fileupload.js │ │ ├── djng-forms.js │ │ ├── djng-rmi.js │ │ ├── djng-urls.js │ │ ├── djng-websocket.js │ │ └── djng.js └── tests │ ├── djangoFormsSpec.js │ ├── djangoRMI.js.deprecated │ └── djangoUrl.js ├── djng ├── __init__.py ├── app_config.py ├── app_settings.py ├── core │ ├── __init__.py │ └── urlresolvers.py ├── forms │ ├── __init__.py │ ├── angular_base.py │ ├── angular_model.py │ ├── angular_validation.py │ ├── fields.py │ └── widgets.py ├── locale │ ├── cs │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── fr │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── middleware.py ├── sekizai_processors.py ├── static │ └── djng │ │ ├── css │ │ ├── bootstrap3.css │ │ ├── fileupload.css │ │ └── styles.css │ │ ├── icons │ │ ├── CREDITS.md │ │ ├── _blank.png │ │ ├── _page.png │ │ ├── aac.png │ │ ├── ai.png │ │ ├── aiff.png │ │ ├── avi.png │ │ ├── bmp.png │ │ ├── c.png │ │ ├── cpp.png │ │ ├── css.png │ │ ├── csv.png │ │ ├── dat.png │ │ ├── dmg.png │ │ ├── doc.png │ │ ├── dotx.png │ │ ├── dwg.png │ │ ├── dxf.png │ │ ├── eps.png │ │ ├── exe.png │ │ ├── file │ │ │ ├── download.svg │ │ │ └── trash.svg │ │ ├── flv.png │ │ ├── gif.png │ │ ├── h.png │ │ ├── hpp.png │ │ ├── html.png │ │ ├── ics.png │ │ ├── image │ │ │ ├── download.svg │ │ │ └── trash.svg │ │ ├── iso.png │ │ ├── java.png │ │ ├── jpg.png │ │ ├── js.png │ │ ├── key.png │ │ ├── less.png │ │ ├── mid.png │ │ ├── mp3.png │ │ ├── mp4.png │ │ ├── mpg.png │ │ ├── odf.png │ │ ├── ods.png │ │ ├── odt.png │ │ ├── otp.png │ │ ├── ots.png │ │ ├── ott.png │ │ ├── pdf.png │ │ ├── php.png │ │ ├── png.png │ │ ├── ppt.png │ │ ├── psd.png │ │ ├── py.png │ │ ├── qt.png │ │ ├── rar.png │ │ ├── rb.png │ │ ├── rtf.png │ │ ├── sass.png │ │ ├── scss.png │ │ ├── sql.png │ │ ├── tga.png │ │ ├── tgz.png │ │ ├── tiff.png │ │ ├── txt.png │ │ ├── wav.png │ │ ├── xls.png │ │ ├── xlsx.png │ │ ├── xml.png │ │ ├── yml.png │ │ └── zip.png │ │ └── js │ │ ├── django-angular.js │ │ └── django-angular.min.js ├── styling │ ├── __init__.py │ └── bootstrap3 │ │ ├── __init__.py │ │ └── forms.py ├── templates │ └── djng │ │ └── forms │ │ └── widgets │ │ ├── bootstrap3 │ │ ├── attrs.html │ │ ├── checkbox.html │ │ ├── checkbox_select.html │ │ ├── date.html │ │ ├── datetime.html │ │ ├── email.html │ │ ├── input.html │ │ ├── number.html │ │ ├── password.html │ │ ├── radio.html │ │ ├── select.html │ │ ├── text.html │ │ ├── textarea.html │ │ ├── time.html │ │ └── url.html │ │ ├── checkbox.html │ │ ├── checkbox_select.html │ │ ├── date.html │ │ ├── datetime.html │ │ ├── email.html │ │ ├── number.html │ │ ├── password.html │ │ ├── radio.html │ │ ├── select.html │ │ ├── text.html │ │ ├── textarea.html │ │ ├── time.html │ │ └── url.html ├── templatetags │ ├── __init__.py │ └── djng_tags.py ├── urls.py └── views │ ├── __init__.py │ ├── crud.py │ ├── mixins.py │ └── upload.py ├── docker-files ├── redis.conf ├── redis.ini ├── uwsgi-emperor.ini ├── uwsgi-runserver.ini ├── uwsgi-websocket.ini ├── wsgi_runserver.py └── wsgi_websocket.py ├── docs ├── Makefile ├── _static │ ├── badge-rtd.png │ └── unbound-form.png ├── angular-form-validation.rst ├── angular-model-form.rst ├── basic-crud-operations.rst ├── changelog.rst ├── conf.py ├── csrf-protection.rst ├── demos.rst ├── forms-set.rst ├── index.rst ├── installation.rst ├── integration.rst ├── remote-method-invocation.rst ├── resolve-dependencies.rst ├── reverse-urls.rst ├── template-sharing.rst ├── three-way-data-binding.rst ├── tutorial-forms.rst └── upload-files.rst ├── examples ├── .coveragerc ├── manage.py ├── package.json ├── pytest.ini ├── requirements.txt └── server │ ├── __init__.py │ ├── context_processors.py │ ├── forms │ ├── __init__.py │ ├── client_validation.py │ ├── combined_validation.py │ ├── forms_set.py │ ├── image_file_upload.py │ ├── model_scope.py │ └── subscribe_form.py │ ├── models │ ├── __init__.py │ ├── image_file_upload.py │ └── testing.py │ ├── settings.py │ ├── static │ ├── css │ │ └── djangular-demo.css │ └── js │ │ ├── djng-tutorial.js │ │ └── three-way-data-binding.js │ ├── templates │ ├── base.html │ ├── client-validation.html │ ├── combined-validation.html │ ├── form-data-valid.html │ ├── forms-set.html │ ├── image-file-upload.html │ ├── model-scope.html │ ├── subscribe-form.html │ └── three-way-data-binding.html │ ├── templatetags │ ├── __init__.py │ └── tutorial_tags.py │ ├── tests │ ├── __init__.py │ ├── sample-image.jpg │ ├── settings.py │ ├── test_crud.py │ ├── test_fileupload.py │ ├── test_forms.py │ ├── test_model_forms.py │ ├── test_postprocessor.py │ ├── test_templatetags.py │ ├── test_urlresolver_view.py │ ├── test_urlresolvers.py │ ├── test_validation.py │ ├── test_validation_forms.py │ ├── test_views.py │ └── urls.py │ ├── tutorial │ ├── client-validation.html │ ├── combined-validation.html │ ├── forms-set.html │ ├── image-file-upload.html │ ├── model-scope.html │ ├── subscribe-form.html │ └── three-way-data-binding.html │ ├── urls.py │ └── views │ ├── __init__.py │ ├── classic_subscribe.py │ ├── client_validation.py │ ├── combined_validation.py │ ├── forms_set.py │ ├── image_file_upload.py │ ├── model_scope.py │ └── threeway_databinding.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = djng 3 | branch = True 4 | 5 | [report] 6 | show_missing = True 7 | omit = 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !docker-files/ 3 | !examples/server/ 4 | !examples/manage.py 5 | !examples/package.json 6 | !examples/requirements.txt 7 | !client/src 8 | !djng 9 | !setup.py 10 | !README.md 11 | !LICENSE.txt 12 | !MANIFEST.in 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /djangular/static/djangular/js/django-angular.* -crlf -diff 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish django-angular 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | name: "Publish release" 11 | runs-on: "ubuntu-latest" 12 | 13 | environment: 14 | name: deploy 15 | 16 | strategy: 17 | matrix: 18 | python-version: ["3.9"] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v3 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | python -m pip install twine 30 | python -m pip install build 31 | - name: Build 🐍 Python 📦 Package 32 | run: | 33 | python -m build --sdist --wheel --outdir dist/ 34 | twine check --strict dist/* 35 | - name: Publish 🐍 Python 📦 Package to PyPI 36 | if: startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.PYPI_API_TOKEN_DJANGO_ANGULAR }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.coverage 4 | *.cache 5 | *~ 6 | .tmp* 7 | .DS_Store 8 | .idea/ 9 | .tox 10 | cdncache/ 11 | dist/ 12 | htmlcov/ 13 | docs/_build/ 14 | workdir/ 15 | node_modules/ 16 | build/ 17 | examples/test.sqlite 18 | client/package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | services: 4 | - xvfb 5 | 6 | language: python 7 | 8 | python: 9 | - 3.6 10 | - 3.7 11 | - 3.8 12 | 13 | env: 14 | - DJANGOVER=django21 15 | - DJANGOVER=django22 16 | - DJANGOVER=django30 17 | - DJANGOVER=django31 18 | 19 | install: 20 | - pip install tox 21 | 22 | matrix: 23 | exclude: 24 | - python: 3.8 25 | env: DJANGOVER=django21 26 | - python: 3.8 27 | env: DJANGOVER=django22 28 | 29 | before_script: 30 | - export CHROME_BIN=chromium-browser 31 | - export DISPLAY=:99.0 32 | - sleep 3 # give xvfb some time to start 33 | 34 | script: 35 | - export TOX_ENV=py${TRAVIS_PYTHON_VERSION/./}-${DJANGOVER} 36 | - tox -r -e "$TOX_ENV" 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to django-angular 2 | ============================== 3 | 4 | As an open source project, django-angular welcomes contributions of many forms. 5 | 6 | Examples of contributions include: 7 | 8 | * Code patches 9 | * Documentation improvements 10 | * Bug reports and patch reviews 11 | * Translations 12 | 13 | 14 | New Translation 15 | =============== 16 | 17 | If you'd like to submit new translations, please use: 18 | 19 | 20 | `cd django-angular/djng` 21 | 22 | `django-admin.py makemessages --locale=fr` (e.g. for french translation) 23 | 24 | 25 | Edit the corresponding file in `locale/fr/LC_MESSAGES/django.po`, commit it and submit your pull request. 26 | 27 | 28 | Code of Conduct 29 | =============== 30 | 31 | As a contributor, you can help us keep the community open and inclusive. 32 | 33 | Please read and follow at least the django [Code of Conduct](https://www.djangoproject.com/conduct/). 34 | 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM awesto/fedora-uwsgi-python:5 2 | 3 | LABEL Description="Run django-angular demo" Maintainer="Jacob Rief " 4 | 5 | # install and configure Redis 6 | RUN dnf install -y redis 7 | RUN mkdir -p /web/redis 8 | COPY docker-files/redis.ini /etc/uwsgi.d/redis.ini 9 | COPY docker-files/redis.conf /etc/redis.conf 10 | RUN chown redis.redis /etc/uwsgi.d/redis.ini 11 | RUN chown -R redis.redis /web/redis 12 | 13 | RUN useradd -M -d /web -g uwsgi -s /bin/bash django 14 | 15 | # install the basic Django package 16 | RUN echo 2 | alternatives --config python 17 | RUN python -V 18 | # RUN pip install --upgrade pip 19 | # RUN pip install --force-reinstall uwsgi 20 | RUN pip install django==1.10.7 21 | 22 | # copy the local django-angular file into a temporary folder 23 | RUN mkdir -p /tmp/django-angular 24 | COPY LICENSE.txt /tmp/django-angular 25 | COPY README.md /tmp/django-angular 26 | COPY MANIFEST.in /tmp/django-angular 27 | COPY setup.py /tmp/django-angular 28 | ADD djng /tmp/django-angular/djng 29 | # and from there install it into the site-package using setup.py 30 | RUN pip install /tmp/django-angular 31 | RUN rm -rf /tmp/django-angular 32 | 33 | # create the example project 34 | RUN mkdir -p /web/workdir/{media,static} 35 | ADD examples/server /web/django-angular-demo/server 36 | ADD client /web/django-angular-demo/client 37 | COPY docker-files/wsgi_runserver.py /web/django-angular-demo/wsgi_runserver.py 38 | COPY docker-files/wsgi_websocket.py /web/django-angular-demo/wsgi_websocket.py 39 | COPY examples/manage.py /web/django-angular-demo/manage.py 40 | COPY examples/package.json /web/django-angular-demo/package.json 41 | COPY examples/requirements.txt /tmp/requirements.txt 42 | RUN pip install -r /tmp/requirements.txt 43 | RUN pip install django-websocket-redis 44 | 45 | # install packages under node_modules/ outside of PyPI 46 | WORKDIR /web/django-angular-demo 47 | RUN npm install 48 | 49 | # add uwsgi.ini file into workdir, so that touching this file restarts the Django server 50 | COPY docker-files/uwsgi-emperor.ini /etc/uwsgi.ini 51 | COPY docker-files/uwsgi-websocket.ini /web/workdir/uwsgi-websocket.ini 52 | RUN ln -s /web/workdir/uwsgi-websocket.ini /etc/uwsgi.d/djangular-websocket.ini 53 | COPY docker-files/uwsgi-runserver.ini /web/workdir/uwsgi-runserver.ini 54 | RUN ln -s /web/workdir/uwsgi-runserver.ini /etc/uwsgi.d/djangular-runserver.ini 55 | 56 | # collect static files 57 | RUN CLIENT_SRC_DIR=/web/django-angular-demo/client/src NODE_MODULES_DIR=/web/django-angular-demo/node_modules DJANGO_STATIC_ROOT=/web/workdir/static ./manage.py collectstatic --noinput 58 | RUN chown -R django.uwsgi /web/{logs,workdir} 59 | 60 | # share media files 61 | VOLUME /web/workdir/media 62 | 63 | # when enabling the CMD disable deamonize in uwsgi.ini 64 | EXPOSE 9002 65 | CMD ["/usr/sbin/uwsgi", "--ini", "/etc/uwsgi.ini"] 66 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jacob Rief 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | include CONTRIBUTIONS.md 4 | include setup.py 5 | recursive-include djng *.py 6 | recursive-include djng/static * 7 | recursive-include djng/templates * 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-angular 2 | 3 | Let Django play well with AngularJS 4 | 5 | [![Build Status](https://travis-ci.org/jrief/django-angular.svg?branch=master)](https://travis-ci.org/jrief/django-angular) 6 | [![PyPI version](https://img.shields.io/pypi/v/django-angular.svg)](https://pypi.python.org/pypi/django-angular) 7 | [![Python versions](https://img.shields.io/pypi/pyversions/django-angular.svg)](https://pypi.python.org/pypi/django-angular) 8 | [![Software license](https://img.shields.io/pypi/l/django-angular.svg)](https://github.com/jrief/django-angular/blob/master/LICENSE-MIT) 9 | [![Twitter Follow](https://img.shields.io/twitter/follow/jacobrief.svg?style=social&label=Jacob+Rief)](https://twitter.com/jacobrief) 10 | 11 | 12 | ## Deprecation Warning: [AngularJS is dead](https://blog.angular.io/discontinued-long-term-support-for-angularjs-cc066b82e65a) 13 | 14 | Since AngularJS is deprecated now, this library shall not be used for new projects anymore. 15 | Instead please proceed with my follow-up project [django-formset](https://github.com/jrief/django-formset). 16 | All the useful features for Django Form validation have been reimplemented in **django-formset**, this time 17 | in vanilla TypeScript and without depending on any third party package. 18 | 19 | 20 | ## What does it offer? 21 | 22 | Add AngularJS directives to Django Forms. This allows to handle client side form validation using 23 | the constraints provided by the server side Form declaration. 24 | 25 | For more information, please visit the [demo site](https://django-angular.awesto.com/form_validation/). 26 | 27 | 28 | ### How to run 29 | 30 | ``` 31 | git clone https://github.com/jrief/django-angular.git django-angular.git 32 | cd django-angular.git 33 | docker build -t django-angular.git . 34 | docker run -d -it -p 9002:9002 django-angular.git 35 | ``` 36 | 37 | Open the application at `http://{docker-host's-ip}:9002/` 38 | 39 | ### Backward Incompatibility 40 | 41 | To be compliant with other libraries such as **djangorestframework**, server-side responses on 42 | rejected forms use error code 422, rather than 200. If you use your own form controllers, adopt 43 | them accordingly. The JSON format used to communicate errors downstream has changed slightly. 44 | 45 | ### New Features 46 | 47 | For a smoother transition path, **django-angular** added two directives in version 2.0: 48 | 49 | ``
...
``, which can be used to upload form 50 | data to the server. It also populates the error fields, in case the server rejected some data. 51 | 52 | ``
...
...
`` 53 | Similar to the above directive, but rather than validating one single form, it validates a 54 | set of forms using one shared endpoint. 55 | 56 | A promise chain has been introduced. Buttons used to submit form data and then proceed with 57 | something else, now can be written as: 58 | 59 | ```` 60 | 61 | 62 | ## Documentation 63 | 64 | Detailed documentation on [ReadTheDocs](http://django-angular.readthedocs.org/en/latest/). 65 | 66 | [Demo](http://django-angular.awesto.com/form_validation/) on how to combine Django with Angular's form validation. 67 | 68 | Please drop me a line, if and where you use this project. 69 | 70 | 71 | ## Features 72 | 73 | * Seamless integration of Django forms with AngularJS controllers. 74 | * Client side form validation for Django forms using AngularJS. 75 | * Let an AngularJS controller call methods in a Django view - kind of Javascript RPCs. 76 | * Manage Django URLs for static controller files. 77 | * Three way data binding to connect AngularJS models with a server side message queue. 78 | * Perform basic CRUD operations. 79 | 80 | ## Examples 81 | 82 | * Implement Filter On Single Page Application using Django & Angular JS https://shriniket.home.blog/2019/09/22/integrate-angular-js-with-django-filter-application/ 83 | 84 | 85 | ## License 86 | 87 | Copyright © 2019 88 | 89 | MIT licensed 90 | -------------------------------------------------------------------------------- /authors-info.yml: -------------------------------------------------------------------------------- 1 | # This file is used to configure the "ghizmo assemble-authors" command. 2 | 3 | header: | 4 | This work is the result of the effort of many people around the world. 5 | Contributors are listed in alphabetical order by GitHub login. 6 | footer: | 7 | Numbers link to commits/issues. 8 | For simplicity, this file is maintained only in English. 9 | If you see inaccuracies or omissions, please file an issue, or edit the authors-info.yml file, regenerate, and file a PR. 10 | exclude: 11 | gitter-badger 12 | 13 | roles: 14 | jrief: original author and maintainer 15 | jkosir: collaborator 16 | adrienbrunet: collaborator 17 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Building django-angular.min.js 2 | 3 | ```shell 4 | cd client 5 | npm install 6 | npm run build 7 | ``` 8 | 9 | if that fails, try to convert line endings using 10 | 11 | ```shell 12 | dos2unix node_modules/concat-glob-cli/index.js 13 | ``` 14 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Karma configuration 4 | module.exports = function(config) { 5 | config.set({ 6 | // frameworks to use 7 | frameworks: ['jasmine'], 8 | 9 | // list of files / patterns to load in the browser 10 | files: [ 11 | 'node_modules/angular/angular.js', 12 | 'node_modules/angular-mocks/angular-mocks.js', 13 | 'src/js/*.js', 14 | 'tests/*.js' 15 | ], 16 | 17 | // test results reporter to use 18 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 19 | reporters: ['progress'], 20 | 21 | // web server port 22 | port: 9090, 23 | 24 | // enable / disable colors in the output (reporters and logs) 25 | colors: true, 26 | 27 | // level of logging 28 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 29 | logLevel: config.LOG_INFO, 30 | 31 | // enable / disable watching file and executing tests whenever any file changes 32 | autoWatch: false, 33 | 34 | // Start these browsers, currently available: 35 | // - Chrome 36 | // - ChromeCanary 37 | // - Firefox 38 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 39 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 40 | // - PhantomJS 41 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 42 | browsers: ['ChromeCanary'], 43 | 44 | // If browser does not capture in given timeout [ms], kill it 45 | captureTimeout: 60000, 46 | 47 | // Continuous Integration mode 48 | // if true, it capture browsers, run tests and exit 49 | singleRun: false 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-angular", 3 | "description": "Let Django play well with AngularJS", 4 | "version": "2.0.0", 5 | "homepage": "https://github.com/jrief/django-angular", 6 | "author": { 7 | "name": "Jacob Rief", 8 | "email": "jacob.rief@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/jrief/django-angular.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/jrief/django-angular/issues" 16 | }, 17 | "keywords": [ 18 | "Django", 19 | "AngularJS" 20 | ], 21 | "license": "MIT", 22 | "devDependencies": { 23 | "angular": "~1.6.6", 24 | "angular-mocks": "~1.6.6", 25 | "concat-glob-cli": "^0.1.0", 26 | "http-server": "^0.9.0", 27 | "jasmine": "^2.8.0", 28 | "jasmine-core": "^2.8.0", 29 | "karma": "^0.13.22", 30 | "karma-chrome-launcher": "^0.2.3", 31 | "karma-firefox-launcher": "^0.1.7", 32 | "karma-jasmine": "^0.3.8", 33 | "karma-junit-reporter": "^0.4.1", 34 | "protractor": "^3.2.2", 35 | "uglify-js": "^3.0.15" 36 | }, 37 | "scripts": { 38 | "prestart": "npm install", 39 | "start": "http-server -a localhost -p 8000 -c-1 ./app", 40 | "pretest": "npm install", 41 | "test": "karma start karma.conf.js", 42 | "test-single-run": "karma start karma.conf.js --single-run", 43 | "preupdate-webdriver": "npm install", 44 | "update-webdriver": "webdriver-manager update", 45 | "preprotractor": "npm run update-webdriver", 46 | "protractor": "protractor e2e-tests/protractor.conf.js", 47 | "concat": "concat-glob-cli -f 'src/js/djng*.js' -o ../djng/static/djng/js/django-angular.js", 48 | "uglify": "uglifyjs ../djng/static/djng/js/django-angular.js -o ../djng/static/djng/js/django-angular.min.js", 49 | "build": "npm run concat && npm run uglify", 50 | "update-index-async": "node -e \"var fs=require('fs'),indexFile='app/index-async.html',loaderFile='app/bower_components/angular-loader/angular-loader.min.js',loaderText=fs.readFileSync(loaderFile,'utf-8').split(/sourceMappingURL=angular-loader.min.js.map/).join('sourceMappingURL=bower_components/angular-loader/angular-loader.min.js.map'),indexText=fs.readFileSync(indexFile,'utf-8').split(/\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/).join('//@@NG_LOADER_START@@\\n'+loaderText+' //@@NG_LOADER_END@@');fs.writeFileSync(indexFile,indexText);\"" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/src/js/djng-fileupload.js: -------------------------------------------------------------------------------- 1 | (function(angular, undefined) { 2 | 'use strict'; 3 | 4 | // module: djng.uploadfiles 5 | // Connect the third party module `ng-file-upload` to django-angular 6 | var fileuploadModule = angular.module('djng.fileupload', ['ngFileUpload']); 7 | 8 | 9 | fileuploadModule.directive('djngFileuploadUrl', ['Upload', function(Upload) { 10 | return { 11 | restrict: 'A', 12 | require: 'ngModel', 13 | link: function(scope, element, attrs, ngModelController) { 14 | ngModelController.$setViewValue({}); 15 | element.data('area_label', element.val()); 16 | if (attrs.currentFile) { 17 | angular.extend(scope.$eval(attrs.ngModel), {current_file: attrs.currentFile}); 18 | element.data('current_file', attrs.currentFile); 19 | element.val(attrs.currentFile.substring(0, attrs.currentFile.indexOf(':'))); 20 | element.addClass('djng-preset'); 21 | } else { 22 | element.addClass('djng-empty'); 23 | } 24 | 25 | scope.uploadFile = function(file, filetype, id, model) { 26 | var data = {'file:0': file, filetype: filetype}, 27 | element = angular.element(document.querySelector('#' + id)); 28 | element.addClass('uploading'); 29 | Upload.upload({ 30 | data: data, 31 | url: attrs.djngFileuploadUrl 32 | }).then(function(response) { 33 | var field = response.data['file:0']; 34 | var cf = element.data('current_file'); 35 | element.removeClass('uploading'); 36 | if (!field) 37 | return; 38 | element.css('background-image', field.url); 39 | element.removeClass('djng-empty'); 40 | element.removeClass('djng-preset'); 41 | element.val(field.file_name); 42 | delete field.url; // we don't want to send back the whole image 43 | angular.extend(scope.$eval(model), field, cf ? {current_file: cf} : {}); 44 | }, function(respose) { 45 | element.removeClass('uploading'); 46 | console.error(respose.statusText); 47 | }); 48 | }; 49 | } 50 | }; 51 | }]); 52 | 53 | 54 | fileuploadModule.directive('djngFileuploadButton', function() { 55 | return { 56 | restrict: 'A', 57 | link: function(scope, element, attrs) { 58 | scope.deleteImage = function(id, _model) { 59 | var model = scope.$eval(_model), 60 | element = angular.element(document.querySelector('#' + id)); 61 | element.css('background-image', 'none'); 62 | element.addClass('djng-empty'); 63 | element.removeClass('djng-preset'); 64 | element.val(element.data('area_label')); 65 | if (model) { 66 | model.temp_name = 'delete'; // tags previous image for deletion 67 | } 68 | }; 69 | } 70 | }; 71 | }); 72 | 73 | })(window.angular); 74 | -------------------------------------------------------------------------------- /client/src/js/djng-rmi.js: -------------------------------------------------------------------------------- 1 | (function(angular, undefined) { 2 | 'use strict'; 3 | 4 | // module: djng.rmi 5 | var djng_rmi_module = angular.module('djng.rmi', []); 6 | 7 | // A simple wrapper to extend the $httpProvider for executing remote methods on the server side 8 | // for Django Views derived from JSONResponseMixin. 9 | // It can be used to invoke GET and POST requests. The return value is the same promise as returned 10 | // by $http.get() and $http.post(). 11 | // Usage: 12 | // djangoRMI.name.method(data).success(...).error(...) 13 | // @param data (optional): If set and @allowed_action was auto, then the call is performed as method 14 | // POST. If data is unset, method GET is used. data must be a valid JavaScript object or undefined. 15 | djng_rmi_module.provider('djangoRMI', function() { 16 | var remote_methods, http; 17 | 18 | this.configure = function(conf) { 19 | remote_methods = conf; 20 | convert_configuration(remote_methods); 21 | }; 22 | 23 | function convert_configuration(obj) { 24 | angular.forEach(obj, function(val, key) { 25 | if (!angular.isObject(val)) 26 | throw new Error('djangoRMI.configure got invalid data'); 27 | if (val.hasOwnProperty('url')) { 28 | // convert config object into function 29 | val.headers['X-Requested-With'] = 'XMLHttpRequest'; 30 | obj[key] = function(data) { 31 | var config = angular.copy(val); 32 | if (config.method === 'POST') { 33 | if (data === undefined) 34 | throw new Error('Calling remote method '+ key +' without data object'); 35 | config.data = data; 36 | } else if (config.method === 'auto') { 37 | if (data === undefined) { 38 | config.method = 'GET'; 39 | } else { 40 | // TODO: distinguish between POST and PUT 41 | config.method = 'POST'; 42 | config.data = data; 43 | } 44 | } 45 | return http(config); 46 | }; 47 | } else { 48 | // continue to examine the values recursively 49 | convert_configuration(val); 50 | } 51 | }); 52 | } 53 | 54 | this.$get = ['$http', function($http) { 55 | http = $http; 56 | return remote_methods; 57 | }]; 58 | }); 59 | 60 | })(window.angular); 61 | -------------------------------------------------------------------------------- /client/src/js/djng-urls.js: -------------------------------------------------------------------------------- 1 | (function (angular, undefined) { 2 | 'use strict'; 3 | /* 4 | module: djng.urls 5 | Provide url reverse resolution functionality for django urls in angular 6 | Usage: djangoUrl.reverse(url_name, args_or_kwargs) 7 | 8 | Examples: 9 | - djangoUrl.reverse('home', [user_id: 2]); 10 | - djangoUrl.reverse('home', [2]); 11 | */ 12 | var djngUrls = angular.module('djng.urls', []); 13 | 14 | djngUrls.provider('djangoUrl', function djangoUrlProvider() { 15 | var reverseUrl = '/angular/reverse/'; 16 | 17 | this.setReverseUrl = function (url) { 18 | reverseUrl = url; 19 | }; 20 | 21 | this.$get = function () { 22 | return new djangoUrl(reverseUrl); 23 | }; 24 | } 25 | ); 26 | 27 | var djangoUrl = function (reverseUrl) { 28 | /* 29 | Url-reversing service 30 | */ 31 | 32 | //Functions from angular.js source, not public available 33 | //See: https://github.com/angular/angular.js/issues/7429 34 | function forEachSorted(obj, iterator, context) { 35 | var keys = sortedKeys(obj); 36 | for (var i = 0; i < keys.length; i++) { 37 | iterator.call(context, obj[keys[i]], keys[i]); 38 | } 39 | return keys; 40 | } 41 | 42 | function sortedKeys(obj) { 43 | var keys = []; 44 | for (var key in obj) { 45 | if (obj.hasOwnProperty(key)) { 46 | keys.push(key); 47 | } 48 | } 49 | return keys.sort(); 50 | } 51 | 52 | function buildUrl(url, params) { 53 | if (!params) return url; 54 | var parts = []; 55 | forEachSorted(params, function (value, key) { 56 | if (value === null || value === undefined) return; 57 | if (angular.isObject(value)) { 58 | value = angular.toJson(value); 59 | } 60 | /* 61 | If value is a string and starts with ':' we don't encode the value to enable parametrized urls 62 | E.g. with .reverse('article',{id: ':id'} we build a url 63 | /angular/reverse/?djng_url_name=article?id=:id, which angular resource can use 64 | https://docs.angularjs.org/api/ngResource/service/$resource 65 | */ 66 | if ((typeof value === 'string' || value instanceof String) && value.lastIndexOf(':', 0) === 0) { 67 | parts.push(encodeURIComponent(key) + '=' + value); 68 | } else { 69 | parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); 70 | } 71 | 72 | }); 73 | return url + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&'); 74 | } 75 | 76 | // Service public interface 77 | this.reverse = function (url_name, args_or_kwargs) { 78 | var url = buildUrl(reverseUrl, {djng_url_name: url_name}); 79 | /* 80 | Django wants arrays in query params encoded the following way: a = [1,2,3] -> ?a=1&a=2$a=3 81 | buildUrl function doesn't natively understand lists in params, so in case of a argument array 82 | it's called iteratively, adding a single parameter with each call 83 | 84 | url = buildUrl(url, {a:1}) -> returns /url?a=1 85 | url = buildUrl(url, {a:2}) -> returns /url?a=1&a=2 86 | ... 87 | */ 88 | if (Array.isArray(args_or_kwargs)) { 89 | forEachSorted(args_or_kwargs, function (value) { 90 | url = buildUrl(url, {'djng_url_args': value}); 91 | }); 92 | return url; 93 | } 94 | /* 95 | If there's a object of keyword arguments, a 'djng_url_kwarg_' prefix is prepended to each member 96 | Then we can directly call the buildUrl function 97 | */ 98 | var params = {}; 99 | forEachSorted(args_or_kwargs, function (value, key) { 100 | params['djng_url_kwarg_' + key] = value; 101 | }); 102 | /* 103 | If params is empty (no kwargs passed) return url immediately 104 | Calling buildUrl with empty params object adds & or ? at the end of query string 105 | E.g. buldUrl('/url/djng_url_name=home', {}) -> /url/djng_url_name=home& 106 | */ 107 | if (angular.equals(params, {})) { // If params is empty, no kwargs passed. 108 | return url; 109 | } 110 | return buildUrl(url, params); 111 | }; 112 | }; 113 | 114 | })(window.angular); 115 | -------------------------------------------------------------------------------- /client/src/js/djng.js: -------------------------------------------------------------------------------- 1 | angular.module('djng', [ 2 | 'djng.forms', 3 | // 'djng.rmi', 4 | 'djng.urls' 5 | ]); -------------------------------------------------------------------------------- /client/tests/djangoFormsSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('unit tests for module djng.forms', function() { 4 | function compileForm($compile, scope, replace_value) { 5 | var template = 6 | '
' + 7 | '' + 8 | '
'; 9 | var form = angular.element(template.replace('{value}', replace_value)); 10 | $compile(form)(scope); 11 | scope.$digest(); 12 | } 13 | 14 | describe('test default form behavior', function() { 15 | var scope; 16 | 17 | beforeEach(inject(function($rootScope) { 18 | scope = $rootScope.$new(); 19 | })); 20 | 21 | describe('on unbound forms', function() { 22 | it('the view value of empty input fields should remain undefined', inject(function($compile) { 23 | compileForm($compile, scope, ''); 24 | expect(scope.valid_form.email_field.$viewValue).toBe(undefined); 25 | })); 26 | }); 27 | 28 | describe('on bound forms', function() { 29 | it('the view value of filled input fields should remain undefined', inject(function($compile) { 30 | compileForm($compile, scope, 'value="john@example.net"'); 31 | expect(scope.valid_form.email_field.$viewValue).toBe(undefined); 32 | })); 33 | }); 34 | }); 35 | 36 | describe('test overridden form behavior', function() { 37 | var scope; 38 | 39 | beforeEach(function() { 40 | // djng's 'form' directive, overrides the behavior of the view value. 41 | module('djng.forms'); 42 | }); 43 | 44 | beforeEach(inject(function($rootScope) { 45 | scope = $rootScope.$new(); 46 | })); 47 | 48 | describe('on unbound forms', function() { 49 | it('the view value of empty input fields should be empty', inject(function($compile) { 50 | compileForm($compile, scope, ''); 51 | expect(scope.valid_form.email_field.$viewValue).toBe(undefined); 52 | })); 53 | }); 54 | 55 | describe('on bound forms', function() { 56 | it('the view value of filled input fields should remain as is', inject(function($compile) { 57 | compileForm($compile, scope, 'value="john@example.net"'); 58 | expect(scope.valid_form.email_field.$viewValue).toBe('john@example.net'); 59 | })); 60 | }); 61 | 62 | }); 63 | 64 | describe('test directive validateDate', function() { 65 | var scope, form; 66 | 67 | beforeEach(function() { 68 | // djng's 'form' directive, overrides the behavior of the view value. 69 | module('djng.forms'); 70 | }); 71 | 72 | beforeEach(inject(function($rootScope) { 73 | scope = $rootScope.$new(); 74 | })); 75 | 76 | beforeEach(inject(function($compile) { 77 | var doc = 78 | '
' + 79 | '' + 80 | '
'; 81 | var element = angular.element(doc); 82 | scope.model = { date: null }; 83 | $compile(element)(scope); 84 | form = scope.form; 85 | })); 86 | 87 | it('to reject 2014/04/01', function() { 88 | form.date_field.$setViewValue('2014/04/01'); 89 | scope.$digest(); 90 | expect(form.date_field.$valid).toBe(false); 91 | }); 92 | 93 | it('to accept 2014-04-01', function() { 94 | form.date_field.$setViewValue('2014-03-01'); 95 | scope.$digest(); 96 | expect(form.date_field.$valid).toBe(true); 97 | }); 98 | 99 | it('to reject 2014-04-31', function() { 100 | form.date_field.$setViewValue('2014-02-29'); 101 | scope.$digest(); 102 | expect(form.date_field.$valid).toBe(false); 103 | }); 104 | }); 105 | 106 | describe('test provider djangoForm', function() { 107 | beforeEach(function() { 108 | module('djng.forms'); 109 | }); 110 | 111 | describe('using manual instantiation', function() { 112 | var scope; 113 | 114 | beforeEach(inject(function($rootScope) { 115 | scope = $rootScope.$new(); 116 | })); 117 | 118 | beforeEach(inject(function($compile) { 119 | var form = angular.element( 120 | '
' + 121 | '' + 122 | '
' 123 | ); 124 | $compile(form)(scope); 125 | scope.$digest(); 126 | })); 127 | 128 | it('should parse the composed names', inject(function(djangoForm) { 129 | expect(djangoForm.getScopePrefix('any_name')).toBe('any_name'); 130 | expect(djangoForm.getScopePrefix('any_name.something')).toBe('any_name'); 131 | expect(djangoForm.getScopePrefix('any_name[\'something\']')).toBe('any_name'); 132 | })); 133 | 134 | }); 135 | }); 136 | 137 | }); 138 | -------------------------------------------------------------------------------- /client/tests/djangoRMI.js.deprecated: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('unit tests for module djng.rmi', function() { 4 | var $httpBackend, djangoRMI; 5 | 6 | beforeEach(function() { 7 | module('djng.rmi'); 8 | }); 9 | 10 | describe('emulating get_current_remote_methods', function() { 11 | beforeEach(function() { 12 | angular.module('testApp', function() {}).config(function(djangoRMIProvider) { 13 | djangoRMIProvider.configure({"foo": {"url": "/straight_methods/", "headers": {"DjNg-Remote-Method": "foo"}, "method": "auto"}, "bar": {"url": "/straight_methods/", "headers": {"DjNg-Remote-Method": "bar"}, "method": "auto"}}); 14 | }); 15 | module('djng.forms', 'testApp'); 16 | }); 17 | 18 | beforeEach(inject(function($injector) { 19 | $httpBackend = $injector.get('$httpBackend'); 20 | })); 21 | 22 | it('should call View method foo using GET', inject(function(djangoRMI) { 23 | $httpBackend.when('GET', '/straight_methods/').respond(200, {success: true}); 24 | djangoRMI.foo().success(function(data) { 25 | expect(data.success).toBe(true); 26 | }); 27 | $httpBackend.flush(); 28 | })); 29 | 30 | it('should call View method foo using POST', inject(function(djangoRMI) { 31 | $httpBackend.when('POST', '/straight_methods/').respond(200, {yeah: 'success'}); 32 | djangoRMI.foo({some: 'data'}).success(function(data) { 33 | expect(data.yeah).toBe('success'); 34 | }); 35 | $httpBackend.flush(); 36 | })); 37 | }); 38 | 39 | describe('emulating get_all_remote_methods', function() { 40 | beforeEach(function() { 41 | angular.module('testApp', function() {}).config(function(djangoRMIProvider) { 42 | djangoRMIProvider.configure({"submethods": {"sub": {"app": {"foo": {"url": "/sub_methods/sub/app/", "headers": {"DjNg-Remote-Method": "foo"}, "method": "auto"}, "bar": {"url": "/sub_methods/sub/app/", "headers": {"DjNg-Remote-Method": "bar"}, "method": "auto"}}}}, "straightmethods": {"foo": {"url": "/straight_methods/", "headers": {"DjNg-Remote-Method": "foo"}, "method": "auto"}, "bar": {"url": "/straight_methods/", "headers": {"DjNg-Remote-Method": "bar"}, "method": "auto"}}}); 43 | }); 44 | module('djng.forms', 'testApp'); 45 | }); 46 | 47 | beforeEach(inject(function($injector) { 48 | $httpBackend = $injector.get('$httpBackend'); 49 | })); 50 | 51 | it('should call View method foo using GET', inject(function(djangoRMI) { 52 | $httpBackend.when('GET', '/sub_methods/sub/app/').respond(200, {foo: null}); 53 | djangoRMI.submethods.sub.app.foo().success(function(data) { 54 | expect(data.foo).toBe(null); 55 | }); 56 | $httpBackend.flush(); 57 | })); 58 | 59 | it('should call View method bar using GET', inject(function(djangoRMI) { 60 | $httpBackend.when('GET', '/sub_methods/sub/app/').respond(200, {bar: 'nothing'}); 61 | djangoRMI.submethods.sub.app.bar().success(function(data) { 62 | expect(data.bar).toBe('nothing'); 63 | }); 64 | $httpBackend.flush(); 65 | })); 66 | 67 | it('should call View method foo using POST', inject(function(djangoRMI) { 68 | $httpBackend.when('POST', '/sub_methods/sub/app/').respond(200, {foo: 'some data'}); 69 | djangoRMI.submethods.sub.app.foo({some: 'data'}).success(function(data) { 70 | expect(data.foo).toBe('some data'); 71 | }); 72 | $httpBackend.flush(); 73 | })); 74 | 75 | it('should call View method bar using POST', inject(function(djangoRMI) { 76 | $httpBackend.when('POST', '/sub_methods/sub/app/').respond(200, {bar: 'other data'}); 77 | djangoRMI.submethods.sub.app.bar({other: 'data'}).success(function(data) { 78 | expect(data.bar).toBe('other data'); 79 | }); 80 | $httpBackend.flush(); 81 | })); 82 | }); 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /client/tests/djangoUrl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('unit tests for module djng.url', function () { 4 | var base_url = '/angular/reverse/'; 5 | var arg_prefix = 'djng_url_args'; 6 | var kwarg_prefix = 'djng_url_kwarg_'; 7 | var url_name_arg = 'djng_url_name'; 8 | 9 | describe("test djangoUrlProvider", function () { 10 | beforeEach(function () { 11 | module('djng.urls'); 12 | }); 13 | 14 | describe("Test provider custom url config", function () { 15 | it('tests the providers internal function', inject(function(djangoUrl) { 16 | expect(djangoUrl.reverse('myapp:home')).toBe('/angular/reverse/?djng_url_name=myapp%3Ahome'); 17 | })); 18 | }); 19 | 20 | }); 21 | 22 | describe('test djangoUrl url resolving', function () { 23 | beforeEach(function () { 24 | module('djng.urls'); 25 | }); 26 | 27 | describe('general url reverser tests', function () { 28 | it('should inject the djangoUrl service without errors', inject(function (djangoUrl) { 29 | expect(djangoUrl.reverse).toBeDefined(); 30 | })); 31 | it('should match the base url', inject(function (djangoUrl) { 32 | expect(djangoUrl.reverse('some:url:name')).toContain(base_url); 33 | })); 34 | }); 35 | 36 | describe('test building urls, url name', function () { 37 | it('should correctly add django\'s url name as query parameter', inject(function (djangoUrl) { 38 | expect(djangoUrl.reverse('home')).toBe(base_url + '?' + url_name_arg + '=home') 39 | })); 40 | it('should urlencode name if it contains :', inject(function (djangoUrl) { 41 | // : urlencoded becomes %3A 42 | expect(djangoUrl.reverse('account:profile')).toBe(base_url + '?' + url_name_arg + '=account%3Aprofile') 43 | })); 44 | it('should encode funny characters in name correctly', inject(function (djangoUrl) { 45 | var urlname = '{6;.-$$+/'; 46 | expect(djangoUrl.reverse(urlname)).toBe(base_url + '?' + url_name_arg + '=' + encodeURIComponent(urlname)); 47 | })); 48 | }); 49 | 50 | describe('test building urls with arguments', function () { 51 | it('should add single argument as query param with arg_prefix', inject(function (djangoUrl) { 52 | expect(djangoUrl.reverse('home', [1])).toBe(base_url + '?' + url_name_arg + '=home&' + arg_prefix + '=1'); 53 | })); 54 | it('should add multiple arguments in correct order', inject(function (djangoUrl) { 55 | expect(djangoUrl.reverse('home', [1, 2, 3])) 56 | .toBe(base_url + '?' + url_name_arg + '=home&' + arg_prefix + '=1&' + arg_prefix + '=2&' + arg_prefix + '=3'); 57 | })); 58 | it('should not urlencode args starting with :', inject(function (djangoUrl) { 59 | expect(djangoUrl.reverse('article', [':id'])).toBe( 60 | base_url + '?' + url_name_arg + '=article&' + arg_prefix + '=:id'); 61 | })); 62 | }); 63 | 64 | describe('test building urls with keyword arguments', function () { 65 | it('should add kwarg as kwarg prefix + kwarg name = kwarg value', inject(function (djangoUrl) { 66 | expect(djangoUrl.reverse('home', {id: '7'})) 67 | .toBe(base_url + '?' + url_name_arg + '=home&' + kwarg_prefix + 'id=7'); 68 | })); 69 | it('should add multiple kwargs', inject(function (djangoUrl) { 70 | expect(djangoUrl.reverse('home', {id: '7', name: 'john'})) 71 | .toBe(base_url + '?' + url_name_arg + '=home&' + kwarg_prefix + 'id=7&' + kwarg_prefix + 'name=john'); 72 | })); 73 | it('should not urlencode kwarg values staring with :', inject(function (djangoUrl) { 74 | expect(djangoUrl.reverse('home', {id: ':id'})) 75 | .toBe(base_url + '?' + url_name_arg + '=home&' + kwarg_prefix + 'id=:id'); 76 | })); 77 | }); 78 | 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /djng/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | See PEP 386 (https://www.python.org/dev/peps/pep-0386/) 3 | 4 | Release logic: 5 | 1. Remove ".devX" from __version__ (below) 6 | 2. Remove ".devX" latest version in docs/changelog.rst 7 | 3. git add djng/__init__.py 8 | 4. git commit -m 'Bump to ' 9 | 5. git push 10 | 6. assure that all tests pass on https://travis-ci.org/jrief/django-angular 11 | 7. git tag 12 | 8. git push --tags 13 | 9. python setup.py sdist upload 14 | 10. bump the version, append ".dev0" to __version__ 15 | 11. Add a new heading to docs/changelog.rst, named ".dev0" 16 | 12. git add djng/__init__.py docs/changelog.rst 17 | 12. git commit -m 'Start with ' 18 | 13. git push 19 | """ 20 | 21 | __version__ = '2.3.1' 22 | 23 | default_app_config = 'djng.app_config.DjangoAngularConfig' 24 | -------------------------------------------------------------------------------- /djng/app_config.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoAngularConfig(AppConfig): 5 | name = 'djng' 6 | 7 | def ready(self): 8 | from django.forms.widgets import RadioSelect 9 | 10 | def id_for_label(self, id_, index=None): 11 | if id_ and index and self.add_id_index: 12 | id_ = '%s_%s' % (id_, index) 13 | return id_ 14 | 15 | RadioSelect.id_for_label = id_for_label 16 | -------------------------------------------------------------------------------- /djng/app_settings.py: -------------------------------------------------------------------------------- 1 | class AppSettings(object): 2 | def _setting(self, name, default=None): 3 | from django.conf import settings 4 | return getattr(settings, name, default) 5 | 6 | @property 7 | def upload_storage(self): 8 | import os 9 | from django.core.files.storage import FileSystemStorage 10 | 11 | media_root = self._setting('MEDIA_ROOT', '') 12 | upload_temp = self._setting('DJNG_UPLOAD_TEMP', 'upload_temp') 13 | return FileSystemStorage(location=os.path.join(media_root, upload_temp)) 14 | 15 | @property 16 | def THUMBNAIL_OPTIONS(self): 17 | """ 18 | Set the size as a 2-tuple for thumbnailed images after uploading them. 19 | """ 20 | from django.core.exceptions import ImproperlyConfigured 21 | 22 | size = self._setting('DJNG_THUMBNAIL_SIZE', (200, 200)) 23 | if not (isinstance(size, (list, tuple)) and len(size) == 2 and isinstance(size[0], int) and isinstance(size[1], int)): 24 | raise ImproperlyConfigured("'DJNG_THUMBNAIL_SIZE' must be a 2-tuple of integers.") 25 | return {'crop': True, 'size': size} 26 | 27 | 28 | import sys 29 | app_settings = AppSettings() 30 | app_settings.__name__ = __name__ 31 | sys.modules[__name__] = app_settings 32 | -------------------------------------------------------------------------------- /djng/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /djng/core/urlresolvers.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | 3 | from django.urls import (get_resolver, get_urlconf, resolve, reverse, NoReverseMatch) 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | try: 7 | from django.utils.module_loading import import_string 8 | except ImportError: 9 | from django.utils.module_loading import import_by_path as import_string 10 | 11 | from djng.views.mixins import JSONResponseMixin 12 | 13 | 14 | def _get_remote_methods_for(view_object, url): 15 | # view_object can be a view class or instance 16 | result = {} 17 | for field in dir(view_object): 18 | member = getattr(view_object, field, None) 19 | if callable(member) and hasattr(member, 'allow_rmi'): 20 | config = { 21 | 'url': url, 22 | 'method': getattr(member, 'allow_rmi'), 23 | 'headers': {'DjNg-Remote-Method': field}, 24 | } 25 | result.update({field: config}) 26 | return result 27 | 28 | 29 | def get_all_remote_methods(resolver=None, ns_prefix=''): 30 | """ 31 | Returns a dictionary to be used for calling ``djangoCall.configure()``, which itself extends the 32 | Angular API to the client, offering him to call remote methods. 33 | """ 34 | if not resolver: 35 | resolver = get_resolver(get_urlconf()) 36 | result = {} 37 | for name in resolver.reverse_dict.keys(): 38 | if not isinstance(name, str): 39 | continue 40 | try: 41 | url = reverse(ns_prefix + name) 42 | resmgr = resolve(url) 43 | ViewClass = import_string('{0}.{1}'.format(resmgr.func.__module__, resmgr.func.__name__)) 44 | if isclass(ViewClass) and issubclass(ViewClass, JSONResponseMixin): 45 | result[name] = _get_remote_methods_for(ViewClass, url) 46 | except (NoReverseMatch, ImproperlyConfigured): 47 | pass 48 | for namespace, ns_pattern in resolver.namespace_dict.items(): 49 | sub_res = get_all_remote_methods(ns_pattern[1], ns_prefix + namespace + ':') 50 | if sub_res: 51 | result[namespace] = sub_res 52 | return result 53 | 54 | 55 | def get_current_remote_methods(view): 56 | if isinstance(view, JSONResponseMixin): 57 | return _get_remote_methods_for(view, view.request.path_info) 58 | -------------------------------------------------------------------------------- /djng/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.forms.forms import DeclarativeFieldsMetaclass, BaseForm 3 | from django.forms.models import BaseModelForm, ModelFormMetaclass 4 | from .angular_base import BaseFieldsModifierMetaclass, NgFormBaseMixin 5 | from .angular_model import NgModelFormMixin 6 | from .angular_validation import NgFormValidationMixin 7 | 8 | 9 | class NgDeclarativeFieldsMetaclass(BaseFieldsModifierMetaclass, DeclarativeFieldsMetaclass): 10 | pass 11 | 12 | 13 | class NgForm(NgFormBaseMixin, BaseForm, metaclass=NgDeclarativeFieldsMetaclass): 14 | """ 15 | Convenience class to be used instead of Django's internal ``forms.Form`` when declaring 16 | a form to be used with AngularJS. 17 | """ 18 | 19 | 20 | class NgModelFormMetaclass(BaseFieldsModifierMetaclass, ModelFormMetaclass): 21 | pass 22 | 23 | 24 | class NgModelForm(NgFormBaseMixin, BaseModelForm, metaclass=NgModelFormMetaclass): 25 | """ 26 | Convenience class to be used instead of Django's internal ``forms.ModelForm`` when declaring 27 | a model form to be used with AngularJS. 28 | """ 29 | -------------------------------------------------------------------------------- /djng/forms/angular_model.py: -------------------------------------------------------------------------------- 1 | from django.forms.utils import ErrorDict 2 | from django.utils.html import format_html 3 | from djng.forms.angular_base import NgFormBaseMixin, SafeTuple 4 | 5 | 6 | class NgModelFormMixin(NgFormBaseMixin): 7 | """ 8 | Add this NgModelFormMixin to every class derived from ``forms.Form``, if that custom ``Form`` 9 | shall be managed through an Angular controller. 10 | It adds attributes ``ng-model``, and optionally ``ng-change``, ``ng-class`` and ``ng-style`` 11 | to each of your input fields. 12 | If form validation fails, the ErrorDict is rewritten in a way, so that the Angular controller 13 | can access the error strings using the same key values as for its models. 14 | """ 15 | add_djng_error = False 16 | 17 | def __init__(self, *args, **kwargs): 18 | self.scope_prefix = kwargs.pop('scope_prefix', getattr(self, 'scope_prefix', None)) 19 | self.ng_directives = {} 20 | for key in list(kwargs.keys()): 21 | if key.startswith('ng_'): 22 | fmtstr = kwargs.pop(key) 23 | self.ng_directives[key.replace('_', '-')] = fmtstr 24 | if hasattr(self, 'Meta') and hasattr(self.Meta, 'ng_models'): 25 | if not isinstance(getattr(self.Meta, 'ng_models'), list): 26 | raise TypeError('Meta.ng_model is not of type list') 27 | elif 'ng-model' not in self.ng_directives: 28 | self.ng_directives['ng-model'] = '%(model)s' 29 | super(NgModelFormMixin, self).__init__(*args, **kwargs) 30 | self.prefix = kwargs.get('prefix') 31 | if self.prefix and self.data: 32 | if self.data.get(self.prefix): 33 | self.data = {self.add_prefix(name): value for (name, value) in self.data.get(self.prefix).items()} 34 | else: 35 | self.data = {name: value for (name, value) in self.data.items() if name.startswith(self.prefix + '.')} 36 | if self.scope_prefix == self.form_name: 37 | raise ValueError("The form's name may not be identical with its scope_prefix") 38 | 39 | def _post_clean(self): 40 | """ 41 | Rewrite the error dictionary, so that its keys correspond to the model fields. 42 | """ 43 | super(NgModelFormMixin, self)._post_clean() 44 | if self._errors and self.prefix: 45 | self._errors = ErrorDict((self.add_prefix(name), value) for name, value in self._errors.items()) 46 | 47 | def get_initial_data(self): 48 | """ 49 | Return a dictionary specifying the defaults for this form. This dictionary can be used to 50 | inject the initial values for an Angular controller using the directive: 51 | ``ng-init={{ thisform.get_initial_data|js|safe }}``. 52 | """ 53 | data = {} 54 | ng_models = hasattr(self, 'Meta') and getattr(self.Meta, 'ng_models', []) or [] 55 | for name, field in self.fields.items(): 56 | if 'ng-model' in self.ng_directives or name in ng_models: 57 | data[name] = self.initial.get(name) if self.initial else field.initial 58 | return data 59 | 60 | def get_field_errors(self, field): 61 | errors = super(NgModelFormMixin, self).get_field_errors(field) 62 | if field.is_hidden: 63 | return errors 64 | identifier = format_html('{0}[\'{1}\']', self.form_name, field.html_name) 65 | errors.append(SafeTuple((identifier, self.field_error_css_classes, '$pristine', '$error.rejected', 'invalid', '$message'))) 66 | return errors 67 | 68 | def non_field_errors(self): 69 | errors = super(NgModelFormMixin, self).non_field_errors() 70 | errors.append(SafeTuple((self.form_name, self.form_error_css_classes, '$pristine', '$error.rejected', 'invalid', '$message'))) 71 | return errors 72 | 73 | def update_widget_attrs(self, bound_field, attrs): 74 | super(NgModelFormMixin, self).update_widget_attrs(bound_field, attrs) 75 | identifier = self.add_prefix(bound_field.name) 76 | ng = { 77 | 'name': bound_field.name, 78 | 'identifier': identifier, 79 | 'model': ('%s[\'%s\']' % (self.scope_prefix, identifier)) if self.scope_prefix else identifier 80 | } 81 | if hasattr(self, 'Meta') and bound_field.name in getattr(self.Meta, 'ng_models', []): 82 | attrs['ng-model'] = ng['model'] 83 | for key, fmtstr in self.ng_directives.items(): 84 | attrs[key] = fmtstr % ng 85 | return attrs 86 | -------------------------------------------------------------------------------- /djng/forms/angular_validation.py: -------------------------------------------------------------------------------- 1 | from django.forms import widgets 2 | from django.utils.html import format_html 3 | from django.utils.encoding import force_text 4 | from .angular_base import NgFormBaseMixin, SafeTuple 5 | 6 | 7 | class NgFormValidationMixin(NgFormBaseMixin): 8 | """ 9 | Add this NgFormValidationMixin to every class derived from forms.Form, which shall be 10 | auto validated using the Angular's validation mechanism. 11 | """ 12 | add_djng_error = True 13 | 14 | def __init__(self, *args, **kwargs): 15 | super(NgFormValidationMixin, self).__init__(*args, **kwargs) 16 | for name, field in self.fields.items(): 17 | # add ng-model to each model field 18 | ng_model = self.add_prefix(name) 19 | field.widget.attrs.setdefault('ng-model', ng_model) 20 | 21 | def get_field_errors(self, bound_field): 22 | """ 23 | Determine the kind of input field and create a list of potential errors which may occur 24 | during validation of that field. This list is returned to be displayed in '$dirty' state 25 | if the field does not validate for that criteria. 26 | """ 27 | errors = super(NgFormValidationMixin, self).get_field_errors(bound_field) 28 | if bound_field.is_hidden: 29 | return errors 30 | identifier = format_html('{0}[\'{1}\']', self.form_name, self.add_prefix(bound_field.name)) 31 | potential_errors = bound_field.field.get_potential_errors() 32 | errors.extend([SafeTuple((identifier, self.field_error_css_classes, '$dirty', pe[0], 'invalid', force_text(pe[1]))) 33 | for pe in potential_errors]) 34 | if not isinstance(bound_field.field.widget, widgets.PasswordInput): 35 | # all valid fields shall display OK tick after changed into dirty state 36 | errors.append(SafeTuple((identifier, self.field_error_css_classes, '$dirty', '$valid', 'valid', ''))) 37 | if bound_field.value(): 38 | # valid bound fields shall display OK tick, even in pristine state 39 | errors.append(SafeTuple((identifier, self.field_error_css_classes, '$pristine', '$valid', 'valid', ''))) 40 | return errors 41 | 42 | def update_widget_attrs(self, bound_field, attrs): 43 | super(NgFormValidationMixin, self).update_widget_attrs(bound_field, attrs) 44 | # transfer error state from bound field to AngularJS validation 45 | errors = [e for e in bound_field.errors if e[3] == '$pristine'] 46 | if errors and self.add_djng_error: 47 | attrs.update({'djng-error': 'bound-field'}) 48 | return attrs 49 | -------------------------------------------------------------------------------- /djng/forms/widgets.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | 3 | from django.contrib.staticfiles.storage import staticfiles_storage 4 | from django.core import signing 5 | from django.forms import widgets 6 | from django.forms.utils import flatatt 7 | from django.utils.safestring import mark_safe 8 | from django.utils.html import format_html 9 | from django.utils.translation import ugettext_lazy as _ 10 | 11 | from djng import app_settings 12 | 13 | 14 | class DropFileWidget(widgets.Widget): 15 | signer = signing.Signer() 16 | 17 | def __init__(self, area_label, fileupload_url, attrs=None): 18 | self.area_label = area_label 19 | self.fileupload_url = fileupload_url 20 | super(DropFileWidget, self).__init__(attrs) 21 | self.filetype = 'file' 22 | 23 | def render(self, name, value, attrs=None, renderer=None): 24 | from django.contrib.staticfiles.storage import staticfiles_storage 25 | 26 | extra_attrs = dict(attrs) 27 | extra_attrs.update({ 28 | 'name': name, 29 | 'class': 'djng-{}-uploader'.format(self.filetype), 30 | 'djng-fileupload-url': self.fileupload_url, 31 | 'ngf-drop': 'uploadFile($file, "{0}", "{id}", "{ng-model}")'.format(self.filetype, **attrs), 32 | 'ngf-select': 'uploadFile($file, "{0}", "{id}", "{ng-model}")'.format(self.filetype, **attrs), 33 | }) 34 | self.update_attributes(extra_attrs, value) 35 | final_attrs = self.build_attrs(self.attrs, extra_attrs=extra_attrs) 36 | elements = [format_html('', flatatt(final_attrs), self.area_label)] 37 | 38 | # add a spinning wheel 39 | spinner_attrs = { 40 | 'class': 'glyphicon glyphicon-refresh glyphicon-spin', 41 | 'ng-cloak': True, 42 | } 43 | elements.append(format_html('', flatatt(spinner_attrs))) 44 | 45 | # add a delete icon 46 | icon_attrs = { 47 | 'src': staticfiles_storage.url('djng/icons/{}/trash.svg'.format(self.filetype)), 48 | 'class': 'djng-btn-trash', 49 | 'title': _("Delete File"), 50 | 'djng-fileupload-button ': True, 51 | 'ng-click': 'deleteImage("{id}", "{ng-model}")'.format(**attrs), 52 | 'ng-cloak': True, 53 | } 54 | elements.append(format_html('', flatatt(icon_attrs))) 55 | 56 | # add a download icon 57 | if value: 58 | download_attrs = { 59 | 'href': value.url, 60 | 'class': 'djng-btn-download', 61 | 'title': _("Download File"), 62 | 'download': True, 63 | 'ng-cloak': True, 64 | } 65 | download_icon = staticfiles_storage.url('djng/icons/{}/download.svg'.format(self.filetype)) 66 | elements.append(format_html('', flatatt(download_attrs), download_icon)) 67 | 68 | return format_html('
{}
', mark_safe(''.join(elements))) 69 | 70 | def update_attributes(self, attrs, value): 71 | if value: 72 | try: 73 | content_type, _ = mimetypes.guess_type(value.file.name) 74 | extension = mimetypes.guess_extension(content_type)[1:] 75 | except (IOError, IndexError, TypeError): 76 | extension = '_blank' 77 | background_url = staticfiles_storage.url('djng/icons/{}.png'.format(extension)) 78 | attrs.update({ 79 | 'style': 'background-image: url({});'.format(background_url), 80 | 'current-file': self.signer.sign(value.name) 81 | }) 82 | 83 | 84 | class DropImageWidget(DropFileWidget): 85 | def __init__(self, area_label, fileupload_url, attrs=None): 86 | super(DropImageWidget, self).__init__(area_label, fileupload_url, attrs=attrs) 87 | self.filetype = 'image' 88 | 89 | def update_attributes(self, attrs, value): 90 | if value: 91 | background_url = self.get_background_url(value) 92 | if background_url: 93 | attrs.update({ 94 | 'style': 'background-image: url({});'.format(background_url), 95 | 'current-file': self.signer.sign(value.name) 96 | }) 97 | 98 | def get_background_url(self, value): 99 | from easy_thumbnails.exceptions import InvalidImageFormatError 100 | from easy_thumbnails.files import get_thumbnailer 101 | 102 | try: 103 | thumbnailer = get_thumbnailer(value) 104 | thumbnail = thumbnailer.get_thumbnail(app_settings.THUMBNAIL_OPTIONS) 105 | return thumbnail.url 106 | except InvalidImageFormatError: 107 | return 108 | -------------------------------------------------------------------------------- /djng/locale/cs/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/locale/cs/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djng/locale/cs/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-02-03 11:02+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: forms/field_mixins.py:35 22 | #, python-format 23 | msgid "Ensure this value has at least %(limit_value)d character" 24 | msgid_plural "Ensure this value has at least %(limit_value)d characters" 25 | msgstr[0] "Tato hodnota musí mít alespoň %(limit_value)d znak" 26 | msgstr[1] "Tato hodnota musí mít alespoň %(limit_value)d znaků" 27 | 28 | #: forms/field_mixins.py:41 29 | #, python-format 30 | msgid "Ensure this value has at most %(limit_value)d character" 31 | msgid_plural "Ensure this value has at most %(limit_value)d characters" 32 | msgstr[0] "Tato hodnota musí mít nejvýše %(limit_value)d znak" 33 | msgstr[1] "Tato hodnota musí mít nejvýše %(limit_value)d znaků" 34 | 35 | #: forms/field_mixins.py:79 36 | msgid "This input self does not contain valid data." 37 | msgstr "Tato položka neobsahuje platná data" 38 | -------------------------------------------------------------------------------- /djng/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djng/locale/de/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-02-03 11:02+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: forms/field_mixins.py:35 22 | #, python-format 23 | msgid "Ensure this value has at least %(limit_value)d character" 24 | msgid_plural "Ensure this value has at least %(limit_value)d characters" 25 | msgstr[0] "Versichern Sie sich, dass dieser Wert mindestens %(limit_value)d Zeichen hat" 26 | msgstr[1] "Versichern Sie sich, dass dieser Wert mindestens %(limit_value)d Zeichen hat" 27 | 28 | #: forms/field_mixins.py:41 29 | #, python-format 30 | msgid "Ensure this value has at most %(limit_value)d character" 31 | msgid_plural "Ensure this value has at most %(limit_value)d characters" 32 | msgstr[0] "Versichern Sie sich, dass dieser Wert höchstens %(limit_value)d Zeichen hat" 33 | msgstr[1] "Versichern Sie sich, dass dieser Wert höchstens %(limit_value)d Zeichen hat" 34 | 35 | #: forms/field_mixins.py:79 36 | msgid "This input self does not contain valid data." 37 | msgstr "Das Eingabefeld enthält keine gültigen Daten" 38 | -------------------------------------------------------------------------------- /djng/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djng/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-02-03 19:15+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: forms/field_mixins.py:35 22 | #, python-format 23 | msgid "Ensure this value has at least %(limit_value)d character" 24 | msgid_plural "Ensure this value has at least %(limit_value)d characters" 25 | msgstr[0] "Asegúrese de que este valor tenga al menos %(limit_value)d caracter" 26 | msgstr[1] "Asegúrese de que este valor tenga al menos %(limit_value)d caracteres" 27 | 28 | #: forms/field_mixins.py:41 29 | #, python-format 30 | msgid "Ensure this value has at most %(limit_value)d character" 31 | msgid_plural "Ensure this value has at most %(limit_value)d characters" 32 | msgstr[0] "Asegúrese de que este valor tenga menos de %(limit_value)d caracter" 33 | msgstr[1] "Asegúrese de que este valor tenga menos de %(limit_value)d caracteres" 34 | 35 | #: forms/field_mixins.py:79 36 | msgid "This input self does not contain valid data." 37 | msgstr "La entrada no contiene datos validos" 38 | -------------------------------------------------------------------------------- /djng/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /djng/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2016-02-03 17:45+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: forms/field_mixins.py:35 22 | #, python-format 23 | msgid "Ensure this value has at least %(limit_value)d character" 24 | msgid_plural "Ensure this value has at least %(limit_value)d characters" 25 | msgstr[0] "Assurez vous que cette valeur comporte au minimum %(limit_value)d caractère" 26 | msgstr[1] "Assurez vous que cette valeur comporte au minimum %(limit_value)d caractères" 27 | 28 | #: forms/field_mixins.py:41 29 | #, python-format 30 | msgid "Ensure this value has at most %(limit_value)d character" 31 | msgid_plural "Ensure this value has at most %(limit_value)d characters" 32 | msgstr[0] "Assurez vous que cette valeur comporte au maximum %(limit_value)d caractère" 33 | msgstr[1] "Assurez vous que cette valeur comporte au maximum %(limit_value)d caractères" 34 | 35 | #: forms/field_mixins.py:79 36 | msgid "This input self does not contain valid data." 37 | msgstr "Ce champs ne contient pas de données valides" 38 | -------------------------------------------------------------------------------- /djng/middleware.py: -------------------------------------------------------------------------------- 1 | from django import http 2 | from django.urls import reverse 3 | from django.utils.http import unquote 4 | try: 5 | from django.utils.deprecation import MiddlewareMixin 6 | except ImportError: 7 | MiddlewareMixin = object 8 | 9 | 10 | class AngularUrlMiddleware(MiddlewareMixin): 11 | """ 12 | If the request path is it should be resolved to actual view, otherwise return 13 | ``None`` and continue as usual. 14 | This must be the first middleware in the MIDDLEWARE_CLASSES tuple! 15 | """ 16 | ANGULAR_REVERSE = '/angular/reverse/' 17 | 18 | def process_request(self, request): 19 | """ 20 | Reads url name, args, kwargs from GET parameters, reverses the url and resolves view function 21 | Returns the result of resolved view function, called with provided args and kwargs 22 | Since the view function is called directly, it isn't ran through middlewares, so the middlewares must 23 | be added manually 24 | The final result is exactly the same as if the request was for the resolved view. 25 | 26 | Parametrized urls: 27 | djangoUrl.reverse can be used with parametrized urls of $resource 28 | In that case the reverse url is something like: /angular/reverse/?djng_url_name=orders&djng_url_kwarg_id=:id 29 | $resource can either replace the ':id' part with say 2 and we can proceed as usual, 30 | reverse with reverse('orders', kwargs={'id': 2}). 31 | 32 | If it's not replaced we want to reverse to url we get a request to url 33 | '/angular/reverse/?djng_url_name=orders&djng_url_kwarg_id=' which 34 | gives a request.GET QueryDict {u'djng_url_name': [u'orders'], u'djng_url_kwarg_id': [u'']} 35 | 36 | In that case we want to ignore the id param and only reverse to url with name 'orders' and no params. 37 | So we ignore args and kwargs that are empty strings. 38 | """ 39 | if request.path == self.ANGULAR_REVERSE: 40 | url_name = request.GET.get('djng_url_name') 41 | url_args = request.GET.getlist('djng_url_args', []) 42 | url_kwargs = {} 43 | 44 | # Remove falsy values (empty strings) 45 | url_args = filter(lambda x: x, url_args) 46 | 47 | # Read kwargs 48 | for param in request.GET: 49 | if param.startswith('djng_url_kwarg_'): 50 | # Ignore kwargs that are empty strings 51 | if request.GET[param]: 52 | url_kwargs[param[15:]] = request.GET[param] # [15:] to remove 'djng_url_kwarg' prefix 53 | 54 | url = unquote(reverse(url_name, args=url_args, kwargs=url_kwargs)) 55 | assert not url.startswith(self.ANGULAR_REVERSE), "Prevent recursive requests" 56 | 57 | # rebuild the request object with a different environ 58 | request.path = request.path_info = url 59 | request.environ['PATH_INFO'] = url 60 | query = request.GET.copy() 61 | for key in request.GET: 62 | if key.startswith('djng_url'): 63 | query.pop(key, None) 64 | request.environ['QUERY_STRING'] = query.urlencode() 65 | 66 | # Reconstruct GET QueryList in the same way WSGIRequest.GET function works 67 | request.GET = http.QueryDict(request.environ['QUERY_STRING']) 68 | -------------------------------------------------------------------------------- /djng/sekizai_processors.py: -------------------------------------------------------------------------------- 1 | """ 2 | To be used in Sekizai's render_block to postprocess AngularJS module dependencies 3 | """ 4 | 5 | import warnings 6 | 7 | from django.conf import settings 8 | from django.core.exceptions import ImproperlyConfigured 9 | from django.utils.html import format_html_join 10 | from django.utils.safestring import mark_safe 11 | 12 | if 'sekizai' not in settings.INSTALLED_APPS: 13 | msg = "Install django-sekizai when using these postprocessors" 14 | raise ImproperlyConfigured(msg) 15 | 16 | 17 | def module_list(context, data, namespace): 18 | warnings.warn("This postprocessor is deprecated. Read on how to resolve AngularJS dependencies using `{% with_data \"ng-requires\" ... %}`") 19 | modules = set(m.strip(' "\'') for m in data.split()) 20 | text = format_html_join(', ', '"{0}"', ((m,) for m in modules)) 21 | return text 22 | 23 | 24 | def module_config(context, data, namespace): 25 | warnings.warn("This postprocessor is deprecated. Read on how to resolve AngularJS dependencies using `{% with_data \"ng-config\" ... %}`") 26 | configs = [(mark_safe(c),) for c in data.split('\n') if c] 27 | text = format_html_join('', '.config({0})', configs) 28 | return text 29 | -------------------------------------------------------------------------------- /djng/static/djng/css/bootstrap3.css: -------------------------------------------------------------------------------- 1 | .djng-form-control-feedback { 2 | position: absolute; 3 | text-align: right; 4 | top: -5px; 5 | right: 10px; 6 | } 7 | .djng-line-spreader { 8 | max-height: 25px; 9 | } 10 | -------------------------------------------------------------------------------- /djng/static/djng/css/fileupload.css: -------------------------------------------------------------------------------- 1 | .drop-box { 2 | position: relative; 3 | width: 200px; 4 | } 5 | .drop-box textarea { 6 | background-color: #F8F8F8; 7 | background-repeat: no-repeat; 8 | border: 1px solid rgb(160, 160, 160); 9 | border-radius: 4px; 10 | width: 100%; 11 | height: auto; 12 | color: rgb(160, 160, 160); 13 | font-weight: bold; 14 | font-size: 14px; 15 | text-shadow: 0 0 10px rgba(0, 0, 0, 0.25); 16 | resize: none; 17 | cursor: copy; 18 | overflow: hidden; 19 | } 20 | .drop-box textarea.djng-empty { 21 | font-size: 18px; 22 | text-align: center; 23 | text-shadow: 0 0 10px rgba(0, 0, 0, 0.25); 24 | border-radius: 12px; 25 | border-width: 6px; 26 | border-style: dashed; 27 | } 28 | .drop-box textarea.djng-empty:hover { 29 | text-shadow: none; 30 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); 31 | } 32 | 33 | 34 | .drop-box textarea.djng-file-uploader { 35 | height: 90px; 36 | } 37 | .drop-box textarea.djng-file-uploader:not(.djng-empty) { 38 | padding: 64px 8px 0 8px; 39 | background-position: 4px 8px; 40 | text-shadow: none; 41 | } 42 | .drop-box textarea.djng-file-uploader:not(.djng-empty):hover { 43 | color: #000; 44 | } 45 | .drop-box textarea.djng-file-uploader.djng-empty { 46 | padding: 16px 24px 0 24px; 47 | } 48 | 49 | 50 | .drop-box textarea.djng-image-uploader { 51 | height: 200px; 52 | } 53 | .drop-box textarea.djng-image-uploader:not(.djng-empty) { 54 | padding: 176px 8px 0 8px; 55 | color: rgba(255, 255, 255, 0.25); 56 | } 57 | .drop-box textarea.djng-image-uploader:not(.djng-empty):hover { 58 | color: rgba(255, 255, 255, 1); 59 | text-shadow: 0 0 10px rgba(0, 0, 0, 1); 60 | } 61 | .drop-box textarea.djng-image-uploader.djng-empty { 62 | padding: 56px 24px 0 24px; 63 | } 64 | 65 | 66 | .drop-box textarea.uploading { 67 | color: transparent; 68 | } 69 | .drop-box textarea.dragover { 70 | background: #f8f8a0; 71 | border-color: #dda; 72 | border-style: solid; 73 | } 74 | 75 | .drop-box .glyphicon-refresh { 76 | position: absolute; 77 | left: 65px; 78 | font-size: 64px; 79 | display: none; 80 | color: rgb(160, 160, 160); 81 | } 82 | .drop-box .glyphicon-spin { 83 | -webkit-animation: spin 2500ms infinite linear; 84 | animation: spin 2500ms infinite linear; 85 | } 86 | .drop-box textarea.djng-image-uploader ~ .glyphicon-refresh { 87 | top: 75px; 88 | } 89 | .drop-box textarea.djng-file-uploader ~ .glyphicon-refresh { 90 | top: 15px; 91 | } 92 | .drop-box textarea.uploading ~ .glyphicon-refresh { 93 | display: inline-block; 94 | } 95 | 96 | .drop-box .djng-btn-trash, .drop-box .djng-btn-download { 97 | position: absolute; 98 | width: 25px; 99 | height: 25px; 100 | opacity: 0.25; 101 | cursor: pointer; 102 | } 103 | .drop-box .djng-btn-trash:hover, .drop-box .djng-btn-download:hover { 104 | opacity: 1; 105 | } 106 | .drop-box textarea.djng-empty ~ .djng-btn-trash { 107 | display: none; 108 | } 109 | .drop-box a.djng-btn-download > img { 110 | display: inline-block; 111 | width: 100%; 112 | height: 100%; 113 | } 114 | .drop-box .djng-btn-trash { 115 | right: 5px; 116 | top: 5px; 117 | } 118 | .drop-box .djng-btn-download { 119 | right: 35px; 120 | top: 5px; 121 | } 122 | .drop-box textarea:not(.djng-preset) ~ .djng-btn-download { 123 | display: none; 124 | } 125 | 126 | @-webkit-keyframes spin { 127 | 0% { 128 | -webkit-transform: rotate(0deg); 129 | transform: rotate(0deg); 130 | } 131 | 100% { 132 | -webkit-transform: rotate(359deg); 133 | transform: rotate(359deg); 134 | } 135 | } 136 | @keyframes spin { 137 | 0% { 138 | -webkit-transform: rotate(0deg); 139 | transform: rotate(0deg); 140 | } 141 | 100% { 142 | -webkit-transform: rotate(359deg); 143 | transform: rotate(359deg); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /djng/static/djng/css/styles.css: -------------------------------------------------------------------------------- 1 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 2 | display: none !important; 3 | } 4 | 5 | form.ng-submitted .ng-invalid, 6 | form .ng-invalid-bound.ng-pristine, 7 | form .ng-invalid.ng-dirty { 8 | border-color: #e9322d; 9 | } 10 | 11 | form.ng-submitted .ng-invalid:focus, 12 | form .ng-invalid-bound.ng-pristine:focus, 13 | form .ng-invalid.ng-dirty:focus { 14 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; 15 | } 16 | label.djng-field-required::after { 17 | content: "\00a0*"; 18 | } 19 | ul.djng-form-errors, ul.djng-field-errors { 20 | display: block; 21 | list-style-type: none; 22 | margin: 0; 23 | padding: 0; 24 | } 25 | ul.djng-form-errors li, ul.djng-field-errors li { 26 | color: #e9322d; 27 | margin-bottom: 5px; 28 | } 29 | ul.djng-field-errors li.invalid::before { 30 | content: "\2716"; 31 | margin-right: 10px; 32 | } 33 | ul.djng-field-errors li.valid::before { 34 | color: #00c900; 35 | content: "\2714"; 36 | } 37 | 38 | .djng-rotate-animate { 39 | animation: spin 2s linear infinite; 40 | } 41 | 42 | @keyframes spin { 100% { transform:rotate(360deg); } } 43 | 44 | button.btn > i { 45 | width: 2em; 46 | display: inline-block; 47 | text-align: center; 48 | } 49 | -------------------------------------------------------------------------------- /djng/static/djng/icons/CREDITS.md: -------------------------------------------------------------------------------- 1 | Credits for filetype icons: 2 | Ilya Zayats 3 | https://github.com/teambox/Free-file-icons 4 | License: MIT 5 | 6 | Credits for button icons: 7 | Damian Kaczmarek 8 | https://github.com/encharm/Font-Awesome-SVG-PNG 9 | License: 10 | The Font Awesome font is licensed under the SIL OFL 1.1: 11 | http://scripts.sil.org/OFL 12 | Font-Awesome-SVG-PNG is licensed under the MIT license 13 | 14 | -------------------------------------------------------------------------------- /djng/static/djng/icons/_blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/_blank.png -------------------------------------------------------------------------------- /djng/static/djng/icons/_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/_page.png -------------------------------------------------------------------------------- /djng/static/djng/icons/aac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/aac.png -------------------------------------------------------------------------------- /djng/static/djng/icons/ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/ai.png -------------------------------------------------------------------------------- /djng/static/djng/icons/aiff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/aiff.png -------------------------------------------------------------------------------- /djng/static/djng/icons/avi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/avi.png -------------------------------------------------------------------------------- /djng/static/djng/icons/bmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/bmp.png -------------------------------------------------------------------------------- /djng/static/djng/icons/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/c.png -------------------------------------------------------------------------------- /djng/static/djng/icons/cpp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/cpp.png -------------------------------------------------------------------------------- /djng/static/djng/icons/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/css.png -------------------------------------------------------------------------------- /djng/static/djng/icons/csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/csv.png -------------------------------------------------------------------------------- /djng/static/djng/icons/dat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/dat.png -------------------------------------------------------------------------------- /djng/static/djng/icons/dmg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/dmg.png -------------------------------------------------------------------------------- /djng/static/djng/icons/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/doc.png -------------------------------------------------------------------------------- /djng/static/djng/icons/dotx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/dotx.png -------------------------------------------------------------------------------- /djng/static/djng/icons/dwg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/dwg.png -------------------------------------------------------------------------------- /djng/static/djng/icons/dxf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/dxf.png -------------------------------------------------------------------------------- /djng/static/djng/icons/eps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/eps.png -------------------------------------------------------------------------------- /djng/static/djng/icons/exe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/exe.png -------------------------------------------------------------------------------- /djng/static/djng/icons/file/download.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /djng/static/djng/icons/file/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /djng/static/djng/icons/flv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/flv.png -------------------------------------------------------------------------------- /djng/static/djng/icons/gif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/gif.png -------------------------------------------------------------------------------- /djng/static/djng/icons/h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/h.png -------------------------------------------------------------------------------- /djng/static/djng/icons/hpp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/hpp.png -------------------------------------------------------------------------------- /djng/static/djng/icons/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/html.png -------------------------------------------------------------------------------- /djng/static/djng/icons/ics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/ics.png -------------------------------------------------------------------------------- /djng/static/djng/icons/image/download.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /djng/static/djng/icons/image/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /djng/static/djng/icons/iso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/iso.png -------------------------------------------------------------------------------- /djng/static/djng/icons/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/java.png -------------------------------------------------------------------------------- /djng/static/djng/icons/jpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/jpg.png -------------------------------------------------------------------------------- /djng/static/djng/icons/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/js.png -------------------------------------------------------------------------------- /djng/static/djng/icons/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/key.png -------------------------------------------------------------------------------- /djng/static/djng/icons/less.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/less.png -------------------------------------------------------------------------------- /djng/static/djng/icons/mid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/mid.png -------------------------------------------------------------------------------- /djng/static/djng/icons/mp3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/mp3.png -------------------------------------------------------------------------------- /djng/static/djng/icons/mp4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/mp4.png -------------------------------------------------------------------------------- /djng/static/djng/icons/mpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/mpg.png -------------------------------------------------------------------------------- /djng/static/djng/icons/odf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/odf.png -------------------------------------------------------------------------------- /djng/static/djng/icons/ods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/ods.png -------------------------------------------------------------------------------- /djng/static/djng/icons/odt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/odt.png -------------------------------------------------------------------------------- /djng/static/djng/icons/otp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/otp.png -------------------------------------------------------------------------------- /djng/static/djng/icons/ots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/ots.png -------------------------------------------------------------------------------- /djng/static/djng/icons/ott.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/ott.png -------------------------------------------------------------------------------- /djng/static/djng/icons/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/pdf.png -------------------------------------------------------------------------------- /djng/static/djng/icons/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/php.png -------------------------------------------------------------------------------- /djng/static/djng/icons/png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/png.png -------------------------------------------------------------------------------- /djng/static/djng/icons/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/ppt.png -------------------------------------------------------------------------------- /djng/static/djng/icons/psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/psd.png -------------------------------------------------------------------------------- /djng/static/djng/icons/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/py.png -------------------------------------------------------------------------------- /djng/static/djng/icons/qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/qt.png -------------------------------------------------------------------------------- /djng/static/djng/icons/rar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/rar.png -------------------------------------------------------------------------------- /djng/static/djng/icons/rb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/rb.png -------------------------------------------------------------------------------- /djng/static/djng/icons/rtf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/rtf.png -------------------------------------------------------------------------------- /djng/static/djng/icons/sass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/sass.png -------------------------------------------------------------------------------- /djng/static/djng/icons/scss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/scss.png -------------------------------------------------------------------------------- /djng/static/djng/icons/sql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/sql.png -------------------------------------------------------------------------------- /djng/static/djng/icons/tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/tga.png -------------------------------------------------------------------------------- /djng/static/djng/icons/tgz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/tgz.png -------------------------------------------------------------------------------- /djng/static/djng/icons/tiff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/tiff.png -------------------------------------------------------------------------------- /djng/static/djng/icons/txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/txt.png -------------------------------------------------------------------------------- /djng/static/djng/icons/wav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/wav.png -------------------------------------------------------------------------------- /djng/static/djng/icons/xls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/xls.png -------------------------------------------------------------------------------- /djng/static/djng/icons/xlsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/xlsx.png -------------------------------------------------------------------------------- /djng/static/djng/icons/xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/xml.png -------------------------------------------------------------------------------- /djng/static/djng/icons/yml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/yml.png -------------------------------------------------------------------------------- /djng/static/djng/icons/zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/static/djng/icons/zip.png -------------------------------------------------------------------------------- /djng/styling/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /djng/styling/bootstrap3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/djng/styling/bootstrap3/__init__.py -------------------------------------------------------------------------------- /djng/styling/bootstrap3/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms.forms import BaseForm 2 | from django.forms.models import BaseModelForm 3 | from djng.forms import NgDeclarativeFieldsMetaclass, NgModelFormMetaclass, NgFormBaseMixin 4 | 5 | 6 | class Bootstrap3FormMixin(object): 7 | field_css_classes = 'form-group has-feedback' 8 | widget_css_classes = 'form-control' 9 | form_error_css_classes = 'djng-form-errors' 10 | field_error_css_classes = 'djng-form-control-feedback djng-field-errors' 11 | label_css_classes = 'control-label' 12 | 13 | def as_div(self): 14 | """ 15 | Returns this form rendered as HTML with
s for each form field. 16 | """ 17 | # wrap non-field-errors into
-element to prevent re-boxing 18 | error_row = '
%s
' 19 | div_element = self._html_output( 20 | normal_row='%(label)s%(field)s%(help_text)s%(errors)s
', 21 | error_row=error_row, 22 | row_ender='
', 23 | help_text_html='%s', 24 | errors_on_separate_row=False) 25 | return div_element 26 | 27 | 28 | class Bootstrap3Form(Bootstrap3FormMixin, NgFormBaseMixin, BaseForm, metaclass=NgDeclarativeFieldsMetaclass): 29 | """ 30 | Convenience class to be used instead of Django's internal ``forms.Form`` when declaring 31 | a form to be used with AngularJS and Bootstrap3 styling. 32 | """ 33 | 34 | 35 | class Bootstrap3ModelForm(Bootstrap3FormMixin, NgFormBaseMixin, BaseModelForm, metaclass=NgModelFormMetaclass): 36 | """ 37 | Convenience class to be used instead of Django's internal ``forms.ModelForm`` when declaring 38 | a model form to be used with AngularJS and Bootstrap3 styling. 39 | """ 40 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/attrs.html: -------------------------------------------------------------------------------- 1 | {% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value }}"{% endif %}{% endif %}{% endfor %}{% if widget.attrs.class %} class="{{ widget.attrs.class }}"{% endif %} -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/checkbox.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/input.html" %}{% if widget.field_label %} {{ widget.field_label }}{% endif %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/checkbox_select.html: -------------------------------------------------------------------------------- 1 | {% with id=widget.attrs.id %} 2 |
{% for group, options, index in widget.optgroups %}{% if group %}

Nested choices not supported yet

{% endif %}{% for option in options %} 3 | {% if wrap_label %}{% endif %}{% if wrap_label %} {{ option.label }}{% endif %} 4 | {% endfor %}{% endfor %}
5 | {% endwith %} -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/date.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/datetime.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/email.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/input.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/number.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/password.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/radio.html: -------------------------------------------------------------------------------- 1 | {% with id=widget.attrs.id %} 2 | {% for group, options, index in widget.optgroups %}{% if group %}

Nested choices not supported yet

{% endif %}{% for option in options %}
3 | {% if wrap_label %}{% endif %}{% if wrap_label %} {{ option.label }}{% endif %} 4 |
{% endfor %} 5 | {% endfor %}{% endwith %} 6 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/select.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/text.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/textarea.html: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/time.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/bootstrap3/url.html: -------------------------------------------------------------------------------- 1 | {% include "djng/forms/widgets/bootstrap3/input.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/checkbox.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/input.html" %}{% if widget.field_label %} {{ widget.field_label }}{% endif %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/checkbox_select.html: -------------------------------------------------------------------------------- 1 | {% with id=widget.attrs.id %} 2 |
{% for group, options, index in widget.optgroups %}{% if group %}

Nested choices not supported yet

{% endif %}{% for option in options %} 3 | {% if wrap_label %}{% endif %}{% if wrap_label %} {{ option.label }}{% endif %} 4 | {% endfor %}{% endfor %}
5 | {% endwith %} -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/date.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/date.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/datetime.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/datetime.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/email.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/email.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/number.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/number.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/password.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/password.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/radio.html: -------------------------------------------------------------------------------- 1 | {% with id=widget.attrs.id %} 2 | {% for group, options, index in widget.optgroups %}{% if group %}

Nested choices not supported yet

{% endif %}{% for option in options %} 3 | {% if wrap_label %}{% endif %}{% if wrap_label %} {{ option.label }}{% endif %} 4 | {% endfor %} 5 | {% endfor %}{% endwith %} 6 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/select.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/select.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/text.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/text.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/textarea.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/textarea.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/time.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/time.html" %} 2 | -------------------------------------------------------------------------------- /djng/templates/djng/forms/widgets/url.html: -------------------------------------------------------------------------------- 1 | {% include "django/forms/widgets/url.html" %} 2 | -------------------------------------------------------------------------------- /djng/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /djng/templatetags/djng_tags.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.template import Library 4 | from django.template.base import Node, NodeList, TextNode, VariableNode 5 | from django.utils.html import format_html 6 | from django.utils.safestring import mark_safe 7 | from django.utils.translation import get_language_from_request 8 | 9 | from djng.core.urlresolvers import get_all_remote_methods, get_current_remote_methods 10 | 11 | 12 | register = Library() 13 | 14 | 15 | @register.simple_tag(name='djng_all_rmi') 16 | def djng_all_rmi(): 17 | """ 18 | Returns a dictionary of all methods for all Views available for this project, marked with the 19 | ``@allow_remote_invocation`` decorator. The return string can be used directly to initialize 20 | the AngularJS provider, such as ``djangoRMIProvider.configure({­% djng_rmi_configs %­});`` 21 | """ 22 | return mark_safe(json.dumps(get_all_remote_methods())) 23 | 24 | 25 | @register.simple_tag(name='djng_current_rmi', takes_context=True) 26 | def djng_current_rmi(context): 27 | """ 28 | Returns a dictionary of all methods for the current View of this request, marked with the 29 | @allow_remote_invocation decorator. The return string can be used directly to initialize 30 | the AngularJS provider, such as ``djangoRMIProvider.configure({­% djng_current_rmi %­});`` 31 | """ 32 | return mark_safe(json.dumps(get_current_remote_methods(context.get('view')))) 33 | 34 | 35 | @register.simple_tag(name='load_djng_urls', takes_context=True) 36 | def djng_urls(context, *namespaces): 37 | raise DeprecationWarning( 38 | "load_djng_urls templatetag is deprecated and has been removed from this version of django-angular." 39 | "Please refer to documentation for updated way to manage django urls in angular.") 40 | 41 | 42 | class AngularJsNode(Node): 43 | def __init__(self, django_nodelist, angular_nodelist, variable): 44 | self.django_nodelist = django_nodelist 45 | self.angular_nodelist = angular_nodelist 46 | self.variable = variable 47 | 48 | def render(self, context): 49 | if self.variable.resolve(context): 50 | return self.angular_nodelist.render(context) 51 | return self.django_nodelist.render(context) 52 | 53 | 54 | @register.tag 55 | def angularjs(parser, token): 56 | """ 57 | Conditionally switch between AngularJS and Django variable expansion for ``{{`` and ``}}`` 58 | keeping Django's expansion for ``{%`` and ``%}`` 59 | 60 | Usage:: 61 | 62 | {% angularjs 1 %} or simply {% angularjs %} 63 | {% process variables through the AngularJS template engine %} 64 | {% endangularjs %} 65 | 66 | {% angularjs 0 %} 67 | {% process variables through the Django template engine %} 68 | {% endangularjs %} 69 | 70 | Instead of 0 and 1, it is possible to use a context variable. 71 | """ 72 | bits = token.contents.split() 73 | if len(bits) < 2: 74 | bits.append('1') 75 | values = [parser.compile_filter(bit) for bit in bits[1:]] 76 | django_nodelist = parser.parse(('endangularjs',)) 77 | angular_nodelist = NodeList() 78 | for node in django_nodelist: 79 | # convert all occurrences of VariableNode into a TextNode using the 80 | # AngularJS double curly bracket notation 81 | if isinstance(node, VariableNode): 82 | # convert Django's array notation into JS array notation 83 | tokens = node.filter_expression.token.split('.') 84 | token = tokens[0] 85 | for part in tokens[1:]: 86 | if part.isdigit(): 87 | token += '[%s]' % part 88 | else: 89 | token += '.%s' % part 90 | node = TextNode('{{ %s }}' % token) 91 | angular_nodelist.append(node) 92 | parser.delete_first_token() 93 | return AngularJsNode(django_nodelist, angular_nodelist, values[0]) 94 | 95 | 96 | @register.simple_tag(name='djng_locale_script', takes_context=True) 97 | def djng_locale_script(context, default_language='en'): 98 | """ 99 | Returns a script tag for including the proper locale script in any HTML page. 100 | This tag determines the current language with its locale. 101 | 102 | Usage: 103 | 104 | or, if used with a default language: 105 | 106 | """ 107 | language = get_language_from_request(context['request']) 108 | if not language: 109 | language = default_language 110 | return format_html('angular-locale_{}.js', language.lower()) 111 | -------------------------------------------------------------------------------- /djng/urls.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from django.urls import reverse 3 | from django.conf.urls import url 4 | from django.http.response import HttpResponsePermanentRedirect 5 | 6 | 7 | warnings.warn("Reversing URL's using urlpatterns is deprecated. Please use the middleware instead", 8 | DeprecationWarning) 9 | 10 | 11 | def angular_reverse(request, *args, **kwargs): 12 | url_name = request.GET.get('djng_url_name') 13 | url_args = request.GET.getlist('djng_url_args', None) 14 | url_kwargs = {} 15 | 16 | prefix = 'djng_url_kwarg_' 17 | for param in request.GET: 18 | if param.startswith(prefix): 19 | url_kwargs[param[len(prefix):]] = request.GET[param] 20 | 21 | url = reverse(url_name, args=url_args, kwargs=url_kwargs) 22 | return HttpResponsePermanentRedirect(url) 23 | 24 | 25 | urlpatterns = [ 26 | url(r'^reverse/$', angular_reverse), 27 | ] 28 | -------------------------------------------------------------------------------- /djng/views/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /djng/views/upload.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import SuspiciousMultipartForm 2 | from django.core import signing 3 | from django.views.generic import View 4 | from django.http import JsonResponse 5 | 6 | from djng import app_settings 7 | from djng.forms.fields import FileField, ImageField 8 | 9 | 10 | class FileUploadView(View): 11 | storage = app_settings.upload_storage 12 | thumbnail_size = app_settings.THUMBNAIL_OPTIONS 13 | signer = signing.Signer() 14 | 15 | def post(self, request, *args, **kwargs): 16 | if request.POST.get('filetype') == 'file': 17 | field = FileField 18 | elif request.POST.get('filetype') == 'image': 19 | field = ImageField 20 | else: 21 | raise SuspiciousMultipartForm("Missing attribute 'filetype' in form data.") 22 | data = {} 23 | for name, file_obj in request.FILES.items(): 24 | data[name] = field.preview(file_obj) 25 | return JsonResponse(data) 26 | -------------------------------------------------------------------------------- /docker-files/redis.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master = true 3 | socket = /run/redis/uwsgi.sock 4 | attach-daemon = /usr/bin/redis-server /etc/redis.conf 5 | -------------------------------------------------------------------------------- /docker-files/uwsgi-emperor.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | uid = uwsgi 3 | gid = uwsgi 4 | plugins = router_uwsgi 5 | pidfile = /run/uwsgi/uwsgi.pid 6 | emperor = /etc/uwsgi.d 7 | stats = /run/uwsgi/stats.sock 8 | emperor-tyrant = true 9 | cap = setgid,setuid 10 | die-on-term = true 11 | offload-threads = 1 12 | logto = /web/logs/uwsgi.log 13 | http-socket = :9002 14 | route = ^/ws uwsgi:/web/workdir/web.socket,0,0 15 | route = ^/ uwsgi:/web/workdir/django.socket,0,0 16 | -------------------------------------------------------------------------------- /docker-files/uwsgi-runserver.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | plugins = python, logfile 3 | chdir = /web/django-angular-demo 4 | umask = 002 5 | socket = /web/workdir/django.socket 6 | env = DJANGO_SETTINGS_MODULE=server.settings 7 | module = wsgi_runserver:application 8 | buffer-size = 32768 9 | env = DJANGO_STATIC_ROOT=/web/workdir/static 10 | static-map = /static=/web/workdir/static 11 | env = DJANGO_MEDIA_ROOT=/web/workdir/media 12 | static-map = /media=/web/workdir/media 13 | post-buffering = 1 14 | processes = 1 15 | threads = 1 16 | logger = file:/web/logs/django-angular.log 17 | -------------------------------------------------------------------------------- /docker-files/uwsgi-websocket.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | umask = 002 3 | plugins = python, gevent, greenlet, logfile 4 | chdir = /web/django-angular-demo 5 | env = DJANGO_SETTINGS_MODULE=server.settings 6 | http-socket = /web/workdir/web.socket 7 | module = wsgi_websocket:application 8 | processes = 1 9 | offload-threads = 2 10 | http-websockets = true 11 | gevent = 1000 12 | -------------------------------------------------------------------------------- /docker-files/wsgi_runserver.py: -------------------------------------------------------------------------------- 1 | from django.core.wsgi import get_wsgi_application 2 | 3 | application = get_wsgi_application() 4 | -------------------------------------------------------------------------------- /docker-files/wsgi_websocket.py: -------------------------------------------------------------------------------- 1 | import gevent.socket 2 | import redis.connection 3 | redis.connection.socket = gevent.socket 4 | from ws4redis.uwsgi_runserver import uWSGIWebsocketServer 5 | application = uWSGIWebsocketServer() 6 | -------------------------------------------------------------------------------- /docs/_static/badge-rtd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/docs/_static/badge-rtd.png -------------------------------------------------------------------------------- /docs/_static/unbound-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/docs/_static/unbound-form.png -------------------------------------------------------------------------------- /docs/csrf-protection.rst: -------------------------------------------------------------------------------- 1 | .. _csrf-protection: 2 | 3 | ===================================== 4 | Cross Site Request Forgery protection 5 | ===================================== 6 | 7 | Ajax requests submitted using method POST are put to a similar risk for 8 | `Cross Site Request Forgeries`_ as HTTP forms. This type of attack occurs when a malicious Web site 9 | is able to invoke an Ajax request onto your Web site. In Django, one should always add the template 10 | tag csrf_token_ to render a hidden input field containing the token, inside each form submitted by 11 | method POST. 12 | 13 | When it comes to making an Ajax request, it normally is not possible to pass that token using a 14 | Javascript object, because scripts usually are static and no secret can be added dynamically. 15 | AngularJS natively supports CSRF protection, only some minor configuration is required to work with 16 | Django. 17 | 18 | 19 | Configure Angular for Django's CSRF protection 20 | ============================================== 21 | 22 | Angular looks for ``XSRF-TOKEN`` cookie and submits it in ``X-XSRF-TOKEN`` http header, while Django 23 | sets ``csrftoken`` cookie and expects ``X-CSRFToken`` http header. All we have to do is change the 24 | name of cookie and header Angular uses. This is best done in ``config`` block: 25 | 26 | 27 | .. code-block:: javascript 28 | 29 | var my_app = angular.module('myApp', [/* dependencies */]).config(function($httpProvider) { 30 | $httpProvider.defaults.xsrfCookieName = 'csrftoken'; 31 | $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken'; 32 | }); 33 | 34 | When using this approach, ensure that the ``CSRF`` cookie is *not* configured as HTTP_ONLY_, 35 | otherwise for security reasons that value can't be accessed from JavaScript. 36 | 37 | Alternatively, if the block used to configure the AngularJS application is rendered using a Django 38 | template, one can add the value of the token directly to the request headers: 39 | 40 | .. code-block:: javascript 41 | 42 | var my_app = angular.module('myApp', [/* dependencies */]).config(function($httpProvider) { 43 | $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token }}'; 44 | $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 45 | } 46 | 47 | .. _Cross Site Request Forgeries: http://www.squarefree.com/securitytips/web-developers.html#CSRF 48 | .. _csrf_token: https://docs.djangoproject.com/en/1.6/ref/templates/builtins/#csrf-token 49 | .. _HTTP_ONLY: http://www.codinghorror.com/blog/2008/08/protecting-your-cookies-httponly.html 50 | .. _method run: http://docs.angularjs.org/api/angular.Module#methods_run 51 | -------------------------------------------------------------------------------- /docs/demos.rst: -------------------------------------------------------------------------------- 1 | .. _demos: 2 | 3 | ================= 4 | Running the demos 5 | ================= 6 | 7 | Shipped with this project, there are four demo pages, showing how to use the AngularJS validation 8 | and data-binding mechanisms in combination with Django forms. Use them as a starting point for your 9 | own application using **django-angular**. 10 | 11 | To run the demos, change into the directory ``examples`` and start the development server: 12 | 13 | .. code-block:: bash 14 | 15 | pip install -r requirements.txt 16 | ./manage.py runserver 17 | 18 | You can also run unit tests: 19 | 20 | .. code-block:: bash 21 | 22 | py.test 23 | 24 | or, even better 25 | 26 | .. code-block:: bash 27 | 28 | tox 29 | 30 | Now, point a browser onto one of 31 | 32 | * http://localhost:8000/classic_form/ 33 | * http://localhost:8000/form_validation/ 34 | * http://localhost:8000/model_scope/ 35 | * http://localhost:8000/combined_validation/ 36 | * http://localhost:8000/threeway_databinding/ 37 | 38 | 39 | Classic Form 40 | ============ 41 | Classic Subscribe Form with no data validation. 42 | 43 | 44 | Form Validation 45 | =============== 46 | 47 | The *Form Validation* demo shows how to implement a Django form with enriched functionality to 48 | add AngularJS's form validation in a DRY manner. This demo combines the classes 49 | ``NgFormValidationMixin`` with Django's ``forms.Form`` . This demo works without an AngularJS 50 | controller. 51 | 52 | 53 | Model Form 54 | ========== 55 | 56 | The *Model Form* demo shows how to combine a Django form with ``NgFormValidationMixin``, which 57 | creates an AngularJS model on the client in a DRY manner. This model, a Plain Old Javascript Object, 58 | then can be used inside an AngularJS controller for all kind of purposes. Using an XMLHttpRequest, 59 | this object can also be sent back to the server and bound to the same form is was created from. 60 | 61 | 62 | Model Form Validation 63 | ===================== 64 | 65 | The *Model Form Validation* shows how to combined both techniques from above, to create an AngularJS 66 | model which additionally is validated on the client. 67 | 68 | 69 | Three-Way Data-Binding 70 | ====================== 71 | 72 | *Three-Way Data-Binding* shows how to combine a Django form with ``NgFormValidationMixin``, so that 73 | the form is synchronized by the server on all browsers accessing the same URL. 74 | 75 | This demo is only available, if the external dependency `Websocket for Redis`_ has been installed. 76 | 77 | .. _Websocket for Redis: https://pypi.python.org/pypi/django-websocket-redis 78 | 79 | 80 | Artificial form constraints 81 | =========================== 82 | 83 | These demos are all based on the same form containing seven different input fields: CharField, 84 | RegexField (twice), EmailField, DateField, IntegerField and FloadField. Each of those fields has 85 | a different constraint: 86 | 87 | * *First name* requires at least 3 characters. 88 | * *Last name* must start with a capital letter. 89 | * *E-Mail* must be a valid address. 90 | * *Phone number* can start with ``+`` and may contain only digits, spaces and dashes. 91 | * *Birth date* must be a valid date. 92 | * *Weight* must be an integer between 42 and 95. 93 | * *Height* must be a float value between 1.48 and 1.95. 94 | 95 | Additionally there are two artificial constraints defined by the server side validation, which for 96 | obvious reasons require a HTTP round trip in order to fail. These are: 97 | 98 | * The full name may not be “John Doe” 99 | * The email address may not end in “@example.com”, “@example.net” or similar. 100 | 101 | If the client bypasses client-side validation by deactivating JavaScript, the server validation 102 | still rejects these error. Using form validation this way, incorrect form values always are rejected 103 | by the server. 104 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-angular documentation master file 2 | 3 | Welcome to django-angular's documentation! 4 | ========================================== 5 | 6 | Django-Angular is a collection of utilities, which aim to ease the integration of Django_ with 7 | AngularJS_ by providing reusable components. 8 | 9 | Project's home 10 | -------------- 11 | Check for the latest release of this project on `Github`_. 12 | 13 | Please report bugs or ask questions using the `Issue Tracker`_. 14 | 15 | For a quick impression, visit the `demo site`_ for this project. 16 | 17 | 18 | Contents 19 | -------- 20 | .. toctree:: 21 | 22 | installation 23 | integration 24 | demos 25 | angular-model-form 26 | angular-form-validation 27 | forms-set 28 | tutorial-forms 29 | upload-files 30 | basic-crud-operations 31 | remote-method-invocation 32 | csrf-protection 33 | template-sharing 34 | reverse-urls 35 | resolve-dependencies 36 | three-way-data-binding 37 | changelog 38 | 39 | 40 | Indices and tables 41 | ------------------ 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | 46 | .. _Django: https://www.djangoproject.com/ 47 | .. _AngularJS: http://angularjs.org/ 48 | .. _Github: https://github.com/jrief/django-angular 49 | .. _Issue Tracker: https://github.com/jrief/django-angular/issues 50 | .. _demo site: https://django-angular.awesto.com/combined_validation/ 51 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation_and_configuration: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | Install **django-angular**. The latest stable release can be found on PyPI 8 | 9 | .. code-block:: bash 10 | 11 | pip install django-angular 12 | 13 | 14 | Change to the root directory of your project and install Node dependencies: 15 | 16 | .. code-block:: bash 17 | 18 | npm init 19 | npm install angular --save 20 | 21 | 22 | Dependencies 23 | ------------ 24 | 25 | **django-angular** has no dependencies to any other Django app, except ``easy-thumbnails`` and 26 | ``Pillow`` if using the image upload feature. AngularJS may be installed through other then ``npm``. However ``pip`` isn't valid in any case. 27 | 28 | Configuration 29 | ============= 30 | 31 | Add ``'djng'`` to the list of ``INSTALLED_APPS`` in your project's ``settings.py`` file 32 | 33 | .. code-block:: python 34 | 35 | INSTALLED_APPS = [ 36 | ... 37 | 'djng', 38 | 'easy_thumbnails', # optional, if ImageField is used 39 | ... 40 | ] 41 | 42 | 43 | Don't forget to define your ``STATIC_ROOT`` and ``STATIC_URL`` properly. Since we load JavaScript 44 | and CSS files directly from our Node dependencies, add that directory to the static files search 45 | path: 46 | 47 | .. code-block:: python 48 | 49 | STATICFILES_DIRS = [ 50 | ('node_modules', os.path.join(BASE_DIR, 'node_modules')), 51 | ] 52 | 53 | From the project's templates, you may refer the AngularJS files as: 54 | 55 | .. code-block:: django 56 | 57 | 50 | 51 | add the module dependency to your application initialization: 52 | 53 | .. code-block:: javascript 54 | 55 | var my_app = angular.module('myApp', [/* other dependencies */, 'djng.websocket']); 56 | 57 | configure the websocket module with a URL prefix of your choice: 58 | 59 | .. code-block:: javascript 60 | 61 | my_app.config(['djangoWebsocketProvider', function(djangoWebsocketProvider) { 62 | // use WEBSOCKET_URI from django settings as the websocket's prefix 63 | djangoWebsocketProvider.setURI('{{ WEBSOCKET_URI }}'); 64 | djangoWebsocketProvider.setHeartbeat({{ WS4REDIS_HEARTBEAT }}); 65 | 66 | // optionally inform about the connection status in the browser's console 67 | djangoWebsocketProvider.setLogLevel('debug'); 68 | }]); 69 | 70 | If you want to bind the data model in one of your AngularJS controllers, you must inject the 71 | provider **djangoWebsocket** into this controller and then attach the websocket to the server. 72 | 73 | .. code-block:: javascript 74 | 75 | my_app.controller('MyController', function($scope, djangoWebsocket) { 76 | djangoWebsocket.connect($scope, 'my_collection', 'foobar', ['subscribe-broadcast', 'publish-broadcast']); 77 | 78 | // use $scope.my_collection as root object for the data which shall be three-way bound 79 | }); 80 | 81 | This creates a websocket attached to the server sides message queue via the module **ws4redis**. 82 | It then shallow watches the properties of the object named ``'my_collection'``, which contains the 83 | model data. It then fires whenever any of the properties change (for arrays, this implies watching 84 | the array items; for object maps, this implies watching the properties). If a change is detected, 85 | it is propagated up to the server. Changes made to the corresponding object on the server side, 86 | are immediately send back to all clients listening on the named facility, referred here as ``foobar``. 87 | 88 | .. note:: This feature is new and experimental, but due to its big potential, it will be regarded 89 | as one of the key features in future versions of **django-angular**. 90 | 91 | .. _two-way data-binding: http://docs.angularjs.org/guide/databinding 92 | .. _django-websocket-redis: https://github.com/jrief/django-websocket-redis 93 | .. _configuration instructions: http://django-websocket-redis.readthedocs.org/en/latest/installation.html 94 | -------------------------------------------------------------------------------- /examples/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = 4 | djangular 5 | [report] 6 | precision = 2 7 | -------------------------------------------------------------------------------- /examples/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 | os.environ.setdefault('DJANGO_STATIC_ROOT', '/web/production/managed/djangular/static') 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-angular", 3 | "version": "1.2.0", 4 | "description": "Demo for django-angular", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Jacob Rief", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jrief/django-angular.git" 14 | }, 15 | "dependencies": { 16 | "angular": "~1.7.8", 17 | "bootstrap": "^3.3.7", 18 | "ng-file-upload": "^12.2.13" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=server.tests.settings 3 | addopts=--tb native --runxfail 4 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | cssselect==1.0.1 2 | django-classy-tags==0.9.0 3 | django-sekizai==1.0.0 4 | easy-thumbnails==2.6.0 5 | Pillow==7.1.0 6 | pluggy==0.12.0 7 | py==1.8.0 8 | pytz==2019.3 9 | redis==3.3.11 10 | six==1.12.0 11 | wsaccel==0.6.2 12 | -------------------------------------------------------------------------------- /examples/server/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/server/context_processors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf import settings 3 | 4 | 5 | def global_context(request): 6 | """ 7 | Adds additional context variables to the default context. 8 | """ 9 | context = { 10 | 'WITH_WS4REDIS': hasattr(settings, 'WEBSOCKET_URL'), 11 | } 12 | return context 13 | -------------------------------------------------------------------------------- /examples/server/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/server/forms/client_validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # start tutorial 4 | from django.forms import widgets 5 | from django.core.exceptions import ValidationError 6 | from djng.forms import fields, NgFormValidationMixin 7 | from djng.styling.bootstrap3.forms import Bootstrap3Form 8 | 9 | 10 | def validate_password(value): 11 | # Just for demo. Do not validate passwords like this! 12 | if value != 'secret': 13 | raise ValidationError('The password is wrong.') 14 | 15 | 16 | class SubscribeForm(NgFormValidationMixin, Bootstrap3Form): 17 | use_required_attribute = False 18 | 19 | CONTINENT_CHOICES = [('am', 'America'), ('eu', 'Europe'), ('as', 'Asia'), ('af', 'Africa'), 20 | ('au', 'Australia'), ('oc', 'Oceania'), ('an', 'Antartica')] 21 | TRAVELLING_BY = [('foot', 'Foot'), ('bike', 'Bike'), ('mc', 'Motorcycle'), ('car', 'Car'), 22 | ('public', 'Public Transportation'), ('train', 'Train'), ('air', 'Airplane')] 23 | NOTIFY_BY = [('email', 'EMail'), ('phone', 'Phone'), ('sms', 'SMS'), ('postal', 'Postcard')] 24 | 25 | first_name = fields.CharField(label='First name', min_length=3, max_length=20) 26 | 27 | last_name = fields.RegexField( 28 | r'^[A-Z][a-z -]?', 29 | label='Last name', 30 | error_messages={'invalid': 'Last names shall start in upper case'}) 31 | 32 | sex = fields.ChoiceField( 33 | choices=(('m', 'Male'), ('f', 'Female')), 34 | widget=widgets.RadioSelect, 35 | error_messages={'invalid_choice': 'Please select your sex'}) 36 | 37 | email = fields.EmailField( 38 | label='E-Mail', 39 | required=True, 40 | help_text='Please enter a valid email address') 41 | 42 | subscribe = fields.BooleanField( 43 | label='Subscribe Newsletter', 44 | initial=False, required=False) 45 | 46 | phone = fields.RegexField( 47 | r'^\+?[0-9 .-]{4,25}$', 48 | label='Phone number', 49 | error_messages={'invalid': 'Phone number have 4-25 digits and may start with +'}) 50 | 51 | birth_date = fields.DateField( 52 | label='Date of birth', 53 | widget=widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'}), 54 | help_text='Allowed date format: yyyy-mm-dd') 55 | 56 | continent = fields.ChoiceField( 57 | label='Living on continent', 58 | choices=CONTINENT_CHOICES, 59 | error_messages={'invalid_choice': 'Please select your continent'}) 60 | 61 | weight = fields.IntegerField( 62 | label='Weight in kg', 63 | min_value=42, 64 | max_value=95, 65 | error_messages={'min_value': 'You are too lightweight'}) 66 | 67 | height = fields.FloatField( 68 | label='Height in meters', 69 | min_value=1.48, 70 | max_value=1.95, 71 | step=0.05, 72 | error_messages={'max_value': 'You are too tall'}) 73 | 74 | traveling = fields.MultipleChoiceField( 75 | label='Traveling by', 76 | choices=TRAVELLING_BY, 77 | help_text='Choose one or more carriers', 78 | required=True) 79 | 80 | notifyme = fields.MultipleChoiceField( 81 | label='Notify by', 82 | choices=NOTIFY_BY, 83 | widget=widgets.CheckboxSelectMultiple, required=True, 84 | help_text='Must choose at least one type of notification') 85 | 86 | annotation = fields.CharField( 87 | label='Annotation', 88 | required=True, 89 | widget=widgets.Textarea(attrs={'cols': '80', 'rows': '3'})) 90 | 91 | agree = fields.BooleanField( 92 | label='Agree with our terms and conditions', 93 | initial=False, 94 | required=True) 95 | 96 | password = fields.CharField( 97 | label='Password', 98 | widget=widgets.PasswordInput, 99 | validators=[validate_password], 100 | help_text='The password is "secret"') 101 | 102 | confirmation_key = fields.CharField( 103 | max_length=40, 104 | required=True, 105 | widget=widgets.HiddenInput(), 106 | initial='hidden value') 107 | -------------------------------------------------------------------------------- /examples/server/forms/combined_validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # start tutorial 4 | from django.core.exceptions import ValidationError 5 | from django.forms import widgets 6 | from djng.forms import fields, NgModelFormMixin, NgFormValidationMixin 7 | from djng.styling.bootstrap3.forms import Bootstrap3Form 8 | 9 | 10 | def validate_password(value): 11 | # Just for demo. Do not validate passwords like this! 12 | if value != "secret": 13 | raise ValidationError("The password is wrong.") 14 | 15 | class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, Bootstrap3Form): 16 | use_required_attribute = False 17 | scope_prefix = 'subscribe_data' 18 | form_name = 'my_form' 19 | 20 | CONTINENT_CHOICES = [('am', 'America'), ('eu', 'Europe'), ('as', 'Asia'), ('af', 'Africa'), 21 | ('au', 'Australia'), ('oc', 'Oceania'), ('an', 'Antartica')] 22 | TRAVELLING_BY = [('foot', 'Foot'), ('bike', 'Bike'), ('mc', 'Motorcycle'), ('car', 'Car'), 23 | ('public', 'Public Transportation'), ('train', 'Train'), ('air', 'Airplane')] 24 | NOTIFY_BY = [('email', 'EMail'), ('phone', 'Phone'), ('sms', 'SMS'), ('postal', 'Postcard')] 25 | 26 | first_name = fields.CharField(label='First name', min_length=3, max_length=20) 27 | 28 | last_name = fields.RegexField( 29 | r'^[A-Z][a-z -]?', 30 | label='Last name', 31 | error_messages={'invalid': 'Last names shall start in upper case'}) 32 | 33 | sex = fields.ChoiceField( 34 | choices=(('m', 'Male'), ('f', 'Female')), 35 | widget=widgets.RadioSelect, 36 | required=True, 37 | error_messages={'invalid_choice': 'Please select your sex'}, 38 | ) 39 | 40 | email = fields.EmailField( 41 | label='E-Mail', 42 | required=True, 43 | help_text='Please enter a valid email address') 44 | 45 | subscribe = fields.BooleanField( 46 | label='Subscribe Newsletter', 47 | initial=False, required=False) 48 | 49 | phone = fields.RegexField( 50 | r'^\+?[0-9 .-]{4,25}$', 51 | label='Phone number', 52 | error_messages={'invalid': 'Phone number have 4-25 digits and may start with +'}) 53 | 54 | birth_date = fields.DateField( 55 | label='Date of birth', 56 | widget=widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'}), 57 | help_text='Allowed date format: yyyy-mm-dd') 58 | 59 | continent = fields.ChoiceField( 60 | label='Living on continent', 61 | choices=CONTINENT_CHOICES, 62 | error_messages={'invalid_choice': 'Please select your continent'}) 63 | 64 | weight = fields.IntegerField( 65 | label='Weight in kg', 66 | min_value=42, 67 | max_value=95, 68 | error_messages={'min_value': 'You are too lightweight'}) 69 | 70 | height = fields.FloatField( 71 | label='Height in meters', 72 | min_value=1.48, 73 | max_value=1.95, 74 | step=0.05, 75 | error_messages={'max_value': 'You are too tall'}) 76 | 77 | traveling = fields.MultipleChoiceField( 78 | label='Traveling by', 79 | choices=TRAVELLING_BY, 80 | help_text='Choose one or more carriers', 81 | required=True) 82 | 83 | notifyme = fields.MultipleChoiceField( 84 | label='Notify by', 85 | choices=NOTIFY_BY, 86 | widget=widgets.CheckboxSelectMultiple, 87 | required=True, 88 | help_text='Must choose at least one type of notification', 89 | ) 90 | 91 | annotation = fields.CharField( 92 | label='Annotation', 93 | required=True, 94 | widget=widgets.Textarea(attrs={'cols': '80', 'rows': '3'})) 95 | 96 | agree = fields.BooleanField( 97 | label='Agree with our terms and conditions', 98 | initial=False, 99 | required=True) 100 | 101 | password = fields.CharField( 102 | label='Password', 103 | widget=widgets.PasswordInput, 104 | validators=[validate_password], 105 | min_length=6, 106 | help_text='The password is "secret"') 107 | 108 | confirmation_key = fields.CharField( 109 | max_length=40, 110 | required=True, 111 | widget=widgets.HiddenInput(), 112 | initial='hidden value') 113 | 114 | def clean(self): 115 | if self.cleaned_data.get('first_name') == 'John' and self.cleaned_data.get('last_name') == 'Doe': 116 | raise ValidationError('The full name "John Doe" is rejected by the server.') 117 | return super(SubscribeForm, self).clean() 118 | 119 | 120 | default_subscribe_data = { 121 | 'first_name': "John", 122 | 'last_name': "Doe", 123 | 'sex': 'm', 124 | 'email': 'john.doe@example.org', 125 | 'phone': '+1 234 567 8900', 126 | 'birth_date': '1975-06-01', 127 | 'continent': 'eu', 128 | 'height': 1.82, 129 | 'weight': 81, 130 | 'traveling': ['bike', 'train'], 131 | 'notifyme': ['email', 'sms'], 132 | 'annotation': "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 133 | 'agree': True, 134 | 'password': '', 135 | } 136 | -------------------------------------------------------------------------------- /examples/server/forms/forms_set.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # start tutorial 4 | import re 5 | from django.core.exceptions import ValidationError 6 | from django.forms import widgets 7 | from djng.forms import fields, NgModelFormMixin, NgFormValidationMixin 8 | from djng.styling.bootstrap3.forms import Bootstrap3Form 9 | 10 | 11 | def validate_full_name(value): 12 | pattern = re.compile(r'^\S+\s+\S+') 13 | if not pattern.match(value): 14 | raise ValidationError("Please enter a first-, followed by a last name.") 15 | 16 | class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, Bootstrap3Form): 17 | scope_prefix = 'subscribe_data' 18 | form_name = 'subscribe_form' 19 | use_required_attribute = False 20 | 21 | sex = fields.ChoiceField( 22 | choices=[('m', 'Male'), ('f', 'Female')], 23 | widget=widgets.RadioSelect, 24 | error_messages={'invalid_choice': 'Please select your sex'}, 25 | ) 26 | 27 | full_name = fields.CharField( 28 | label='Full name', 29 | validators=[validate_full_name], 30 | help_text='Must consist of a first- and a last name', 31 | ) 32 | 33 | def clean(self): 34 | if self.cleaned_data.get('full_name', '').lower() == 'john doe': 35 | raise ValidationError('The full name "John Doe" is rejected by the server.') 36 | return super(SubscribeForm, self).clean() 37 | 38 | class AddressForm(NgModelFormMixin, NgFormValidationMixin, Bootstrap3Form): 39 | scope_prefix = 'address_data' 40 | form_name = 'address_form' 41 | use_required_attribute = False 42 | 43 | street_name = fields.CharField(label='Street name') 44 | -------------------------------------------------------------------------------- /examples/server/forms/image_file_upload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # start tutorial 4 | from djng.forms import fields, NgModelFormMixin, NgFormValidationMixin 5 | from djng.styling.bootstrap3.forms import Bootstrap3Form 6 | 7 | 8 | class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, Bootstrap3Form): 9 | scope_prefix = 'subscribe_data' 10 | form_name = 'my_form' 11 | use_required_attribute = False 12 | 13 | full_name = fields.CharField( 14 | label='Full name', 15 | min_length=3, 16 | max_length=99, 17 | required=True, 18 | ) 19 | 20 | avatar = fields.ImageField( 21 | label='Photo of yourself', 22 | required=True, 23 | ) 24 | 25 | permit = fields.FileField( 26 | label='Your permit as PDF', 27 | accept='application/pdf', 28 | required=False, 29 | ) 30 | 31 | def clean_avatar(self): 32 | """ 33 | For instance, here you can move the temporary file stored in 34 | `self.cleaned_data['avatar'].file` to a permanent location. 35 | """ 36 | self.cleaned_data['avatar'].file 37 | -------------------------------------------------------------------------------- /examples/server/forms/model_scope.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # start tutorial 4 | from django.core.exceptions import ValidationError 5 | from django.forms import widgets 6 | 7 | from djng.forms import fields, NgModelFormMixin 8 | from djng.styling.bootstrap3.forms import Bootstrap3Form 9 | 10 | 11 | def validate_password(value): 12 | # Just for demo. Do not validate passwords like this! 13 | if value != 'secret': 14 | raise ValidationError('The password is wrong.') 15 | 16 | 17 | class SubscribeForm(NgModelFormMixin, Bootstrap3Form): 18 | use_required_attribute = False 19 | scope_prefix = 'subscribe_data' 20 | form_name = 'my_form' 21 | 22 | CONTINENT_CHOICES = [('am', 'America'), ('eu', 'Europe'), ('as', 'Asia'), ('af', 'Africa'), 23 | ('au', 'Australia'), ('oc', 'Oceania'), ('an', 'Antartica')] 24 | TRAVELLING_BY = [('foot', 'Foot'), ('bike', 'Bike'), ('mc', 'Motorcycle'), ('car', 'Car'), 25 | ('public', 'Public Transportation'), ('train', 'Train'), ('air', 'Airplane')] 26 | NOTIFY_BY = [('email', 'EMail'), ('phone', 'Phone'), ('sms', 'SMS'), ('postal', 'Postcard')] 27 | 28 | first_name = fields.CharField(label='First name', min_length=3, max_length=20) 29 | 30 | last_name = fields.RegexField( 31 | r'^[A-Z][a-z -]?', 32 | label='Last name', 33 | error_messages={'invalid': 'Last names shall start in upper case'}) 34 | 35 | sex = fields.ChoiceField( 36 | choices=(('m', 'Male'), ('f', 'Female')), 37 | widget=widgets.RadioSelect, 38 | error_messages={'invalid_choice': 'Please select your sex'}) 39 | 40 | email = fields.EmailField( 41 | label='E-Mail', 42 | required=True, 43 | help_text='Please enter a valid email address') 44 | 45 | subscribe = fields.BooleanField( 46 | label='Subscribe Newsletter', 47 | initial=False, required=False) 48 | 49 | phone = fields.RegexField( 50 | r'^\+?[0-9 .-]{4,25}$', 51 | label='Phone number', 52 | error_messages={'invalid': 'Phone number have 4-25 digits and may start with +'}) 53 | 54 | birth_date = fields.DateField( 55 | label='Date of birth', 56 | widget=widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'}), 57 | help_text='Allowed date format: yyyy-mm-dd') 58 | 59 | continent = fields.ChoiceField( 60 | label='Living on continent', 61 | choices=CONTINENT_CHOICES, 62 | error_messages={'invalid_choice': 'Please select your continent'}) 63 | 64 | weight = fields.IntegerField( 65 | label='Weight in kg', 66 | min_value=42, 67 | max_value=95, 68 | error_messages={'min_value': 'You are too lightweight'}) 69 | 70 | height = fields.FloatField( 71 | label='Height in meters', 72 | min_value=1.48, 73 | max_value=1.95, 74 | step=0.05, 75 | error_messages={'max_value': 'You are too tall'}) 76 | 77 | traveling = fields.MultipleChoiceField( 78 | label='Traveling by', 79 | choices=TRAVELLING_BY, 80 | help_text='Choose one or more carriers', 81 | required=True) 82 | 83 | notifyme = fields.MultipleChoiceField( 84 | label='Notify by', 85 | choices=NOTIFY_BY, 86 | widget=widgets.CheckboxSelectMultiple, required=True, 87 | help_text='Must choose at least one type of notification') 88 | 89 | annotation = fields.CharField( 90 | label='Annotation', 91 | required=True, 92 | widget=widgets.Textarea(attrs={'cols': '80', 'rows': '3'})) 93 | 94 | agree = fields.BooleanField( 95 | label='Agree with our terms and conditions', 96 | initial=False, 97 | required=True) 98 | 99 | password = fields.CharField( 100 | label='Password', 101 | widget=widgets.PasswordInput, 102 | validators=[validate_password], 103 | help_text='The password is "secret"') 104 | 105 | confirmation_key = fields.CharField( 106 | max_length=40, 107 | required=True, 108 | widget=widgets.HiddenInput(), 109 | initial='hidden value') 110 | 111 | def clean(self): 112 | if self.cleaned_data.get('first_name', '').lower() == 'john' \ 113 | and self.cleaned_data.get('last_name', '').lower() == 'doe': 114 | raise ValidationError('The full name "John Doe" is rejected by the server.') 115 | return super(SubscribeForm, self).clean() 116 | -------------------------------------------------------------------------------- /examples/server/forms/subscribe_form.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # start tutorial 4 | from django.forms import widgets 5 | from django.core.exceptions import ValidationError 6 | from djng.forms import fields 7 | from djng.styling.bootstrap3.forms import Bootstrap3Form 8 | 9 | 10 | def validate_password(value): 11 | # Just for demo. Do not validate passwords like this! 12 | if value != 'secret': 13 | raise ValidationError('The password is wrong.') 14 | 15 | 16 | class SubscribeForm(Bootstrap3Form): 17 | use_required_attribute = False 18 | 19 | CONTINENT_CHOICES = [('am', 'America'), ('eu', 'Europe'), ('as', 'Asia'), ('af', 'Africa'), 20 | ('au', 'Australia'), ('oc', 'Oceania'), ('an', 'Antartica')] 21 | TRAVELLING_BY = [('foot', 'Foot'), ('bike', 'Bike'), ('mc', 'Motorcycle'), ('car', 'Car'), 22 | ('public', 'Public Transportation'), ('train', 'Train'), ('air', 'Airplane')] 23 | NOTIFY_BY = [('email', 'EMail'), ('phone', 'Phone'), ('sms', 'SMS'), ('postal', 'Postcard')] 24 | 25 | first_name = fields.CharField( 26 | label='First name', 27 | min_length=3, 28 | max_length=20) 29 | 30 | last_name = fields.RegexField( 31 | r'^[A-Z][a-z -]?', 32 | label='Last name', 33 | error_messages={'invalid': 'Last names shall start in upper case'}) 34 | 35 | sex = fields.ChoiceField( 36 | choices=(('m', 'Male'), ('f', 'Female')), 37 | widget=widgets.RadioSelect, 38 | error_messages={'invalid_choice': 'Please select your sex'}) 39 | 40 | email = fields.EmailField( 41 | label='E-Mail', 42 | required=True, 43 | help_text='Please enter a valid email address') 44 | 45 | subscribe = fields.BooleanField( 46 | label='Subscribe Newsletter', 47 | initial=False, required=False) 48 | 49 | phone = fields.RegexField( 50 | r'^\+?[0-9 .-]{4,25}$', 51 | label='Phone number', 52 | error_messages={'invalid': 'Phone number have 4-25 digits and may start with +'}) 53 | 54 | birth_date = fields.DateField( 55 | label='Date of birth', 56 | widget=widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'}), 57 | help_text='Allowed date format: yyyy-mm-dd') 58 | 59 | continent = fields.ChoiceField( 60 | label='Living on continent', 61 | choices=CONTINENT_CHOICES, 62 | error_messages={'invalid_choice': 'Please select your continent'}) 63 | 64 | weight = fields.IntegerField( 65 | label='Weight in kg', 66 | min_value=42, 67 | max_value=95, 68 | error_messages={'min_value': 'You are too lightweight'}) 69 | 70 | height = fields.FloatField( 71 | label='Height in meters', 72 | min_value=1.48, 73 | max_value=1.95, 74 | step=0.05, 75 | error_messages={'max_value': 'You are too tall'}) 76 | 77 | traveling = fields.MultipleChoiceField( 78 | label='Traveling by', 79 | choices=TRAVELLING_BY, 80 | help_text='Choose one or more carriers', 81 | required=True) 82 | 83 | notifyme = fields.MultipleChoiceField( 84 | label='Notify by', 85 | choices=NOTIFY_BY, 86 | widget=widgets.CheckboxSelectMultiple, 87 | help_text='Must choose at least one type of notification') 88 | 89 | annotation = fields.CharField( 90 | label='Annotation', 91 | required=True, 92 | widget=widgets.Textarea(attrs={'cols': '80', 'rows': '3'})) 93 | 94 | agree = fields.BooleanField( 95 | label='Agree with our terms and conditions', 96 | initial=False, 97 | required=True) 98 | 99 | password = fields.CharField( 100 | label='Password', 101 | widget=widgets.PasswordInput, 102 | validators=[validate_password], 103 | help_text='The password is "secret"') 104 | 105 | confirmation_key = fields.CharField( 106 | max_length=40, 107 | required=True, 108 | widget=widgets.HiddenInput(), 109 | initial='hidden value') 110 | -------------------------------------------------------------------------------- /examples/server/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/examples/server/models/__init__.py -------------------------------------------------------------------------------- /examples/server/models/image_file_upload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # start tutorial 4 | from django.db import models 5 | from djng.forms import NgModelFormMixin, NgFormValidationMixin 6 | from djng.styling.bootstrap3.forms import Bootstrap3ModelForm 7 | 8 | 9 | class SubscribeUser(models.Model): 10 | full_name = models.CharField( 11 | "Full name", 12 | max_length=99) 13 | 14 | avatar = models.ImageField("Avatar", blank=False, null=True) 15 | 16 | permit = models.FileField("Permit", blank=True, null=True) 17 | 18 | 19 | class SubscribeForm(NgModelFormMixin, NgFormValidationMixin, Bootstrap3ModelForm): 20 | use_required_attribute = False 21 | scope_prefix = 'subscribe_data' 22 | form_name = 'my_form' 23 | 24 | class Meta: 25 | model = SubscribeUser 26 | fields = ['full_name', 'avatar', 'permit'] 27 | -------------------------------------------------------------------------------- /examples/server/models/testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from django.db import models 4 | 5 | 6 | class DummyModel(models.Model): 7 | name = models.CharField(max_length=255) 8 | model2 = models.ForeignKey('DummyModel2', on_delete=models.CASCADE) 9 | timefield = models.DateTimeField(default=datetime.datetime.now) 10 | 11 | 12 | class DummyModel2(models.Model): 13 | name = models.CharField(max_length=255) 14 | 15 | 16 | class SimpleModel(models.Model): 17 | name = models.CharField(max_length=50) 18 | email = models.EmailField(unique=True) 19 | 20 | 21 | class M2MModel(models.Model): 22 | dummy_models = models.ManyToManyField(DummyModel2) 23 | -------------------------------------------------------------------------------- /examples/server/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | # Django settings for unit test project. 4 | import os 5 | 6 | DEBUG = True 7 | 8 | PROJECT_DIR = os.path.dirname(__file__) 9 | 10 | APP_DIR = os.path.abspath(os.path.join(PROJECT_DIR, os.pardir, os.pardir)) 11 | 12 | ALLOWED_HOSTS = ['*'] 13 | 14 | ALLOWED_HOSTS = ['*'] 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': 'test.sqlite', 20 | }, 21 | } 22 | 23 | SITE_ID = 1 24 | 25 | ROOT_URLCONF = 'server.urls' 26 | 27 | SECRET_KEY = 'secret' 28 | 29 | INSTALLED_APPS = [ 30 | 'django.contrib.auth', 31 | 'django.contrib.contenttypes', 32 | 'django.contrib.sessions', 33 | 'django.contrib.admin', 34 | 'django.contrib.staticfiles', 35 | 'easy_thumbnails', 36 | 'sekizai', 37 | 'djng', 38 | 'server', 39 | ] 40 | 41 | USE_L10N = True 42 | 43 | LANGUAGE_CODE = 'en' 44 | 45 | LANGUAGES = ( 46 | ('en', 'English'), 47 | ('de', 'Deutsch'), 48 | ('es', 'Español'), 49 | ('fr', 'Français'), 50 | ('it', 'Italiano'), 51 | ('ru', 'Русский'), 52 | ) 53 | 54 | MIDDLEWARE_CLASSES = ( 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware' 57 | ) 58 | 59 | MEDIA_ROOT = os.environ.get('DJANGO_MEDIA_ROOT', os.path.join(APP_DIR, 'workdir/media')) 60 | 61 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 62 | # trailing slash. 63 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 64 | MEDIA_URL = '/media/' 65 | 66 | # Absolute path to the directory that holds static files. 67 | # Example: "/home/media/media.lawrence.com/static/" 68 | STATIC_ROOT = os.environ.get('DJANGO_STATIC_ROOT', os.path.join(APP_DIR, 'workdir/static')) 69 | 70 | # URL that handles the static files served from STATIC_ROOT. 71 | # Example: "http://media.lawrence.com/static/" 72 | STATIC_URL = '/static/' 73 | 74 | STATICFILES_DIRS = ( 75 | os.environ.get('CLIENT_SRC_DIR', os.path.join(APP_DIR, 'client/src')), 76 | ('node_modules', os.environ.get('NODE_MODULES_DIR', os.path.join(APP_DIR, 'examples/node_modules'))), 77 | ) 78 | 79 | # FORM_RENDERER = 'djng.forms.renderers.DjangoAngularBootstrap3Templates' 80 | 81 | TEMPLATES = [ 82 | { 83 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 84 | 'DIRS': [], 85 | 'APP_DIRS': True, 86 | 'OPTIONS': { 87 | 'context_processors': [ 88 | 'django.contrib.auth.context_processors.auth', 89 | 'django.template.context_processors.debug', 90 | 'django.template.context_processors.i18n', 91 | 'django.template.context_processors.media', 92 | 'django.template.context_processors.static', 93 | 'django.template.context_processors.tz', 94 | 'django.template.context_processors.request', 95 | 'django.contrib.messages.context_processors.messages', 96 | 'sekizai.context_processors.sekizai', 97 | 'server.context_processors.global_context', 98 | ], 99 | }, 100 | }, 101 | ] 102 | 103 | TIME_ZONE = 'Europe/Berlin' 104 | 105 | LOGGING = { 106 | 'version': 1, 107 | 'disable_existing_loggers': False, 108 | 'formatters': { 109 | 'simple': { 110 | 'format': '[%(asctime)s %(module)s] %(levelname)s: %(message)s' 111 | }, 112 | }, 113 | 'handlers': { 114 | 'console': { 115 | 'level': 'DEBUG', 116 | 'class': 'logging.StreamHandler', 117 | 'formatter': 'simple', 118 | }, 119 | }, 120 | 'loggers': { 121 | 'django': { 122 | 'handlers': ['console'], 123 | 'level': 'INFO', 124 | 'propagate': True, 125 | }, 126 | }, 127 | } 128 | 129 | # if package django-websocket-redis is installed, some more tests can be be added 130 | try: 131 | import ws4redis 132 | 133 | INSTALLED_APPS.append('ws4redis') 134 | 135 | for template in TEMPLATES: 136 | template['OPTIONS']['context_processors'].append('ws4redis.context_processors.default') 137 | 138 | # This setting is required to override the Django's main loop, when running in 139 | # development mode, such as ./manage runserver 140 | WSGI_APPLICATION = 'ws4redis.django_runserver.application' 141 | 142 | # URL that distinguishes websocket connections from normal requests 143 | WEBSOCKET_URL = '/ws/' 144 | 145 | # Set the number of seconds each message shall persist 146 | WS4REDIS_EXPIRE = 3600 147 | 148 | WS4REDIS_HEARTBEAT = '--heartbeat--' 149 | 150 | WS4REDIS_PREFIX = 'djangular' 151 | 152 | except ImportError: 153 | pass 154 | -------------------------------------------------------------------------------- /examples/server/static/js/djng-tutorial.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var my_app = angular.module('djangular-demo'); 5 | 6 | my_app.controller('TutorialCtrl', function($scope) { 7 | $scope.setTab = function(tab) { 8 | $scope.activeTab = tab; 9 | }; 10 | 11 | $scope.tabClass = function(tab) { 12 | if (angular.isUndefined($scope.activeTab) && $scope.tabList.length > 0) { 13 | $scope.activeTab = $scope.tabList[0]; 14 | } 15 | return $scope.activeTab === tab ? 'active' : ''; 16 | }; 17 | }); 18 | 19 | })(); -------------------------------------------------------------------------------- /examples/server/static/js/three-way-data-binding.js: -------------------------------------------------------------------------------- 1 | angular.module('djangular-demo') 2 | .controller('MyWebsocketCtrl', function($scope, djangoWebsocket) { 3 | djangoWebsocket.connect($scope, 'subscribe_data', 'subscribe_data', ['subscribe-broadcast', 'publish-broadcast']); 4 | }); 5 | -------------------------------------------------------------------------------- /examples/server/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static djng_tags sekizai_tags tutorial_tags %} 2 | 3 | 4 | 5 | {% block form_title %}Django-Angular Demo{% endblock %} 6 | 7 | {% render_block "css" %} 8 | 9 | 10 | 16 | 17 | 18 | {% addtoblock "css" %}{% endaddtoblock %} 19 | {% addtoblock "css" %}{% endaddtoblock %} 20 | {% addtoblock "css" %}{% endaddtoblock %} 21 | {% addtoblock "css" %}{% endaddtoblock %} 22 | 23 | {% addtoblock "js" %}{% endaddtoblock %} 24 | 25 | 26 | 35 | 36 |
37 |
38 | 65 |
66 | {% block main-content %}{% endblock %} 67 |
68 |
69 |
70 | 71 | {% render_block "js" %} 72 | 73 | 80 | 81 | {% block addtoblock %} 82 | 83 | {% endblock %} 84 | 85 | 86 | Fork me on GitHub 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /examples/server/templates/client-validation.html: -------------------------------------------------------------------------------- 1 | {% extends "subscribe-form.html" %} 2 | {% load tutorial_tags %} 3 | 4 | {% block main-intro %} 5 |

Client-Side Form Validation

6 |

Form validation with AngularJS using only a Django Form

7 |

This example shows how to let AngularJS validate input fields from a Django Form in a DRY manner.

8 | {% endblock main-intro %} 9 | 10 | {% block form_tag %}name="{{ form.form_name }}" method="post" action="." novalidate{% endblock %} 11 | 12 | {% block form_submission %} 13 |

14 | Submission using a POST request:
15 | 16 |

17 |

18 | Test submission of invalid POST data:
19 | 20 |

21 | {% endblock %} 22 | 23 | {% block main-tutorial %} 24 |

The Django forms.Form class offers many possibilities to validate a given form. 25 | This for obvious reasons is done on the server. However, while typing, it is unacceptable to send 26 | the form's content to the server for continuous validation. Therefore, adding client side form 27 | validation is a good idea and commonly used. But since this validation easily can be bypassed by a 28 | malicious client, the same validation has to occur a second time, when the server receives the 29 | form's data for final processing.

30 |

With django-angular, we can handle this without having to re-implement any 31 | client side form validation. Apart from initializing the AngularJS application, no JavaScript is 32 | required to enable this useful feature.

33 | 34 |

35 | {% endblock main-tutorial %} 36 | 37 | {% block main-sample-code %} 38 | {% autoescape off %} 39 |
{% pygments "forms/client_validation.py" %}
40 |
{% pygments "views/client_validation.py" %}
41 |
{% pygments "tutorial/client-validation.html" %}
42 | {% endautoescape %} 43 |

Here the differences to the Classic Subscription example is, that the 44 | Form now additionally must inherit from the mixin class NgFormValidationMixin. 45 | Furthermore, the browsers internal Form validation must be disabled. This is achieved by adding 46 | the property novalidate to the Form's HTML element.

47 | {% endblock main-sample-code %} 48 | -------------------------------------------------------------------------------- /examples/server/templates/form-data-valid.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block main-content %} 4 |

Thank's for submission

5 |

The entered data was valid!

6 |

This page is reached through a redirect initiated by the success handler of the submitted form page.

7 | {% endblock main-content %} 8 | -------------------------------------------------------------------------------- /examples/server/templates/image-file-upload.html: -------------------------------------------------------------------------------- 1 | {% extends "model-scope.html" %} 2 | {% load static sekizai_tags djng_tags tutorial_tags %} 3 | 4 | {% block addtoblock %} 5 | {{ block.super }} 6 | {% addtoblock "css" %}{% endaddtoblock %} 7 | {% addtoblock "js" %}{% endaddtoblock %} 8 | {% addtoblock "js" %}{% endaddtoblock %} 9 | {% add_data "ng-requires" "djng.fileupload" %} 10 | 11 | {% addtoblock "js" %}{% endaddtoblock %} 12 | {% add_data "ng-requires" "djng.urls" %} 13 | 14 | {% addtoblock "ng-config" %}['$httpProvider', function($httpProvider) { $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token }}'; }]{% endaddtoblock %} 15 | {% endblock addtoblock %} 16 | 17 | {% block main-intro %} 18 |

Image and File Uploads

19 |

New in 1.1 Drag and drop images or files into a form

20 |

This example shows how to upload an image or file to the server using AngularJS with Ajax.

21 | {% endblock main-intro %} 22 | 23 | {% block form_tag %}name="{{ form.form_name }}" djng-endpoint="." ng-model-options="{allowInvalid: true}" novalidate{% endblock %} 24 | 25 | {% block form_submission %} 26 |

27 | 31 |

32 | {% endblock %} 33 | 34 | {% block form_foot %} 35 | {% verbatim %} 36 |
Scope:
37 |
subscribe_data = {{ subscribe_data | json }}
38 | {% endverbatim %} 39 | {% endblock form_foot %} 40 | 41 | {% block main-tutorial %} 42 |

If we want to upload an image or file to the server through an Ajax request, we have to divide 43 | the submission into two steps. This is because browsers can not serialize submitted file payload to 44 | JSON. Instead, we first must upload the image or file to the server encoded as 45 | multipart/form-data. The server then creates a temporary copy of the uploaded image 46 | or file and returns a reference to it. This temporary reference is stored inside a hidden field 47 | of our form.

48 |

When the complete form is submitted by the client, only that reference to the temporary file 49 | will be sent through Ajax. The server then moves the previously uploaded copy of that file into 50 | its MEDIA_ROOT directory and stores its location inside a model field.

51 | 52 |

53 | {% endblock main-tutorial %} 54 | 55 | {% block main-sample-code %} 56 | {% autoescape off %} 57 |
{% pygments "forms/image_file_upload.py" %}
58 |
{% pygments "views/image_file_upload.py" %}
59 |
{% pygments "tutorial/image-file-upload.html" %}
60 |
{% pygments "models/image_file_upload.py" %}
61 | {% endautoescape %} 62 | 63 |

A form accepting files and images without a permanent storage does not make 64 | much sense. Therefore you normally would create a Django model using an django.models.ImageField 65 | and/or django.models.FileField. From this model, you can create a form inheriting from 66 | django.forms.ModelForm, as usual. Image and file fields then are replaced by their 67 | AngularJS enabled counterparts, enabling drag & drop and immediate image preview.
68 | A sample implementation is shown in the last tab labeled Model.

69 | {% endblock main-sample-code %} 70 | -------------------------------------------------------------------------------- /examples/server/templates/subscribe-form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static sekizai_tags tutorial_tags %} 3 | 4 | {% block addtoblock %} 5 | {{ block.super }} 6 | {% addtoblock "js" %}{% endaddtoblock %} 7 | {% add_data "ng-requires" "djng.forms" %} 8 | {% endblock %} 9 | 10 | {% block main-content %} 11 | 12 | {% block main-intro %} 13 |

Django's Form Submission

14 |

Classic Form Submission in an AngularJS context

15 |

This example shows how to use a classic Django Form inside an AngularJS application.

16 | {% endblock main-intro %} 17 | 18 |
19 | 20 |
21 | {% csrf_token %} 22 |
23 |
24 | {{ form.as_div }} 25 |
26 |
27 |
28 |
29 | {% block form_submission %} 30 | 31 | {% endblock %} 32 |
33 |
34 |
35 |
36 | {% block form_foot %}{% endblock %} 37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |

How does it work?

45 | 46 | {% block main-tutorial %} 47 |

By inheriting from the mixin class Bootstrap3FormMixin, Django renders the form in 48 | a way, compatible with Twitter Bootstrap. Here the correct CSS classes are added to the 49 | <input> elements, which are embedded inside 50 | <div class="form-group"> containers. If you omit Bootstrap3FormMixin, 51 | then the Form is rendered, as Django would by default.

52 |

When this form is rejected by the server, the list of errors is rendered using AngularJS's built 53 | in Form Controller using the 54 | directive ng-show="formname.fieldname.$pristine". This in contrast to Django's 55 | internal behavior has the advantage, that the field's error message disappears as soon as the 56 | user starts typing.

57 |

Passwords can, for obvious reasons only be validated by the server. Here for demonstration 58 | purpose, this is performed by the password field itself, but don't do this in a productive 59 | environment!

60 | 61 |

62 | {% endblock main-tutorial %} 63 | 64 | 69 | 70 | {% block main-sample-code %} 71 | {% autoescape off %} 72 |
{% pygments "forms/subscribe_form.py" %}
73 |
{% pygments "views/classic_subscribe.py" %}
74 |
{% pygments "tutorial/subscribe-form.html" %}
75 | {% endautoescape %} 76 |

Use this setting, if you want your forms behave the way intended by Django. 77 | Here the only exception is, that errors from a previous and failed form validation disappear, as 78 | soon as the user changes that field.
In this setting, AngularJS adds a dummy 79 | ng-model attribute to each input field.

80 | {% endblock main-sample-code %} 81 | 82 |
83 | 84 | {% endblock main-content %} 85 | -------------------------------------------------------------------------------- /examples/server/templates/three-way-data-binding.html: -------------------------------------------------------------------------------- 1 | {% extends "subscribe-form.html" %} 2 | {% load static sekizai_tags tutorial_tags %} 3 | 4 | {% block form_title %}Django Forms with three-way data-binding{% endblock %} 5 | 6 | {% block addtoblock %} 7 | {{ block.super }} 8 | {% addtoblock "js" %}{% endaddtoblock %} 9 | {% add_data "ng-requires" "djng.websocket" %} 10 | {% addtoblock "ng-config" %}['djangoWebsocketProvider', function(djangoWebsocketProvider) { djangoWebsocketProvider.setURI('{{ WEBSOCKET_URI }}'); djangoWebsocketProvider.setLogLevel('debug'); djangoWebsocketProvider.setHeartbeat({{ WS4REDIS_HEARTBEAT }}); }]{% endaddtoblock %} 11 | 12 | {% endblock addtoblock %} 13 | 14 | {% block main-intro %} 15 |

Django Forms with three-way data-binding

16 |

Point a second browser onto the same URL and observe form synchronization

17 |

This example shows, how to propagate the form's model-scope to a foreign browser using Websockets.

18 | {% endblock main-intro %} 19 | 20 | {% block form_tag %}ng-model-options="{allowInvalid: true}" novalidate ng-controller="MyWebsocketCtrl"{% endblock %} 21 | 22 | {% block form_foot %} 23 | {% verbatim %} 24 |
MyWebsocketCtrl's scope:
25 |
subscribe_data = {{ subscribe_data | json }}
26 | {% endverbatim %} 27 | {% endblock %} 28 | 29 | {% block form_submission %}{% endblock %} 30 | 31 | {% block main-tutorial %} 32 |

With django-angular and the additional Django app 33 | django-websocket-redis, one can extend 34 | two-way data-binding to propagate all changes on a model, back and forward with a corresponding 35 | object stored on the server. This means, that the server “sees” whenever the model changes on the client 36 | and can by itself, modify values on the client side at any time, without having the client 37 | to poll for new messages.

38 |

This can be useful, when the server wants to inform the clients about asynchronous events such 39 | as sport results, chat messages or multi-player game events.

40 |

In this example no submit buttons are available, because the server receives the Form's data on 41 | every change event. Apart from initializing the angular module, the only JavaScript code required 42 | for this example, is the statement djangoWebsocket.connect, which bi-directionally 43 | binds the object subscribe_data from our Controller's $scope object, to an 44 | equally named data bucket inside the remote Redis datastore.

45 | 46 |

47 | {% endblock main-tutorial %} 48 | 49 | {% block main-sample-code %} 50 | {% autoescape off %} 51 |
{% pygments "forms/subscribe_form.py" %}
52 |
{% pygments "views/threeway_databinding.py" %}
53 |
{% pygments "tutorial/three-way-data-binding.html" %}
54 |
{% pygments "static/js/three-way-data-binding.js" %}
55 | {% endautoescape %} 56 |

Note that AngularJS directives are configured inside HTML, since only Django 57 | templates can expand server variables.

58 | {% endblock main-sample-code %} 59 | -------------------------------------------------------------------------------- /examples/server/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/examples/server/templatetags/__init__.py -------------------------------------------------------------------------------- /examples/server/templatetags/tutorial_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import os 4 | from django import template 5 | from django.conf import settings 6 | from django.urls import reverse 7 | from pygments import highlight 8 | from pygments.lexers import PythonLexer 9 | from pygments.lexers.templates import HtmlDjangoLexer 10 | from pygments.lexers.web import JavascriptLexer 11 | from pygments.formatters import HtmlFormatter 12 | 13 | register = template.Library() 14 | 15 | 16 | @register.simple_tag(name='component', takes_context=True) 17 | def component(context, component): 18 | the_component = component.as_component() 19 | html = the_component.dispatch(context['request']) 20 | return html 21 | 22 | 23 | @register.simple_tag 24 | def active(request, url): 25 | if request.path.startswith(reverse(url)): 26 | return 'active' 27 | return '' 28 | 29 | 30 | @register.simple_tag 31 | def pygments(filename): 32 | fqfn = os.path.abspath(os.path.join(settings.PROJECT_DIR, filename)) 33 | with open(fqfn, 'r') as f: 34 | readlines = f.readlines() 35 | startfrom = 0 36 | prevline = True 37 | content = [] 38 | for lno, line in enumerate(readlines): 39 | if 'start tutorial' in line: 40 | startfrom = lno + 1 41 | if 'end tutorial' in line: 42 | break 43 | if bool(line) and not line.isspace(): 44 | content.append(line) 45 | prevline = True 46 | else: 47 | if prevline: 48 | content.append(line) 49 | prevline = False 50 | code = ''.join(content[startfrom:]) 51 | if filename.endswith('.py'): 52 | lexer = PythonLexer() 53 | elif filename.endswith('.html'): 54 | lexer = HtmlDjangoLexer() 55 | elif filename.endswith('.js'): 56 | lexer = JavascriptLexer() 57 | return highlight(code, lexer, HtmlFormatter()) 58 | -------------------------------------------------------------------------------- /examples/server/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/server/tests/sample-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrief/django-angular/6bf46956bfcb23de6babfadc455d958e07b119b9/examples/server/tests/sample-image.jpg -------------------------------------------------------------------------------- /examples/server/tests/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | """Django settings for unit test project.""" 4 | import os 5 | 6 | DEBUG = True 7 | 8 | BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.sqlite3', 13 | 'NAME': 'test.sqlite', 14 | }, 15 | } 16 | 17 | SITE_ID = 1 18 | 19 | ROOT_URLCONF = 'server.urls' 20 | 21 | SECRET_KEY = 'secret' 22 | 23 | INSTALLED_APPS = [ 24 | 'django.contrib.auth', 25 | 'django.contrib.contenttypes', 26 | 'django.contrib.sessions', 27 | 'django.contrib.admin', 28 | 'django.contrib.staticfiles', 29 | 'easy_thumbnails', 30 | 'sekizai', 31 | 'djng', 32 | 'server', 33 | ] 34 | 35 | USE_L10N = True 36 | 37 | # Absolute path to the directory that holds media. 38 | # Example: "/home/media/media.lawrence.com/" 39 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 40 | 41 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 42 | # trailing slash if there is a path component (optional in other cases). 43 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 44 | MEDIA_URL = '/media/' 45 | 46 | # Absolute path to the directory that holds static files. 47 | # Example: "/home/media/media.lawrence.com/static/" 48 | STATIC_ROOT = os.environ.get('DJANGO_STATIC_ROOT', '') 49 | 50 | # URL that handles the static files served from STATIC_ROOT. 51 | # Example: "http://media.lawrence.com/static/" 52 | STATIC_URL = '/static/' 53 | 54 | STATICFILES_DIRS = ( 55 | os.path.join(BASE_DIR, 'client', 'src'), 56 | ) 57 | 58 | # FORM_RENDERER = 'djng.forms.renderers.DjangoAngularTemplates' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.i18n', 70 | 'django.template.context_processors.media', 71 | 'django.template.context_processors.static', 72 | 'django.template.context_processors.tz', 73 | 'django.template.context_processors.request', 74 | 'django.contrib.messages.context_processors.messages', 75 | 'server.context_processors.global_context', 76 | ], 77 | }, 78 | }, 79 | ] 80 | 81 | TIME_ZONE = 'Europe/Berlin' 82 | 83 | LOGGING = { 84 | 'version': 1, 85 | 'disable_existing_loggers': False, 86 | 'formatters': { 87 | 'simple': { 88 | 'format': '[%(asctime)s %(module)s] %(levelname)s: %(message)s' 89 | }, 90 | }, 91 | 'handlers': { 92 | 'console': { 93 | 'level': 'DEBUG', 94 | 'class': 'logging.StreamHandler', 95 | 'formatter': 'simple', 96 | }, 97 | }, 98 | 'loggers': { 99 | 'django': { 100 | 'handlers': ['console'], 101 | 'level': 'INFO', 102 | 'propagate': True, 103 | }, 104 | }, 105 | } 106 | 107 | # if package django-websocket-redis is installed, some more tests can be be added 108 | try: 109 | import ws4redis 110 | 111 | INSTALLED_APPS.append('ws4redis') 112 | 113 | for template in TEMPLATES: 114 | template["OPTIONS"]["context_processors"].append('ws4redis.context_processors.default') 115 | 116 | # This setting is required to override the Django's main loop, when running in 117 | # development mode, such as ./manage runserver 118 | WSGI_APPLICATION = 'ws4redis.django_runserver.application' 119 | 120 | # URL that distinguishes websocket connections from normal requests 121 | WEBSOCKET_URL = '/ws/' 122 | 123 | # Set the number of seconds each message shall persist 124 | WS4REDIS_EXPIRE = 3600 125 | 126 | WS4REDIS_HEARTBEAT = '--heartbeat--' 127 | 128 | WS4REDIS_PREFIX = 'djangular' 129 | 130 | except ImportError: 131 | pass 132 | -------------------------------------------------------------------------------- /examples/server/tests/test_fileupload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os, json 5 | 6 | from django.conf import settings 7 | from django.urls import reverse 8 | from django.core import signing 9 | from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile 10 | from django.test import override_settings, TestCase 11 | from django.test.client import Client 12 | 13 | from pyquery.pyquery import PyQuery 14 | 15 | from djng import app_settings 16 | from djng.forms import NgModelFormMixin, NgForm 17 | from djng.forms.fields import ImageField 18 | 19 | 20 | class TestUploadForm(NgModelFormMixin, NgForm): 21 | scope_prefix = 'my_data' 22 | form_name = 'my_form' 23 | 24 | avatar = ImageField() 25 | 26 | 27 | 28 | class FileUploadTest(TestCase): 29 | signer = signing.Signer() 30 | storage = app_settings.upload_storage 31 | 32 | def tearDown(self): 33 | try: 34 | os.remove(os.path.join(settings.MEDIA_ROOT, 'upload_temp/sample-image.jpg')) 35 | except: 36 | pass 37 | 38 | def upload_image(self): 39 | client = Client() 40 | upload_filename = os.path.join(os.path.dirname(__file__), 'sample-image.jpg') 41 | with open(upload_filename, 'rb') as fp: 42 | upload_url = reverse('fileupload') 43 | response = client.post(upload_url, {'file:0': fp, 'filetype': 'image'}) 44 | self.assertEqual(response.status_code, 200) 45 | return json.loads(response.content.decode('utf-8')) 46 | 47 | def test_upload(self): 48 | content = self.upload_image() 49 | self.assertTrue('file:0' in content) 50 | self.assertTrue(content['file:0']['url'].startswith('url(data:application/octet-stream;base64,/9j/4AAQSkZJRgABA')) 51 | self.assertEqual(content['file:0']['file_name'], 'sample-image.jpg') 52 | self.assertEqual(content['file:0']['content_type'], 'image/jpeg') 53 | self.assertEqual(self.signer.unsign(content['file:0']['temp_name']), 'sample-image.jpg') 54 | 55 | def test_render_widget(self): 56 | form = TestUploadForm() 57 | htmlsource = form.as_p() 58 | dom = PyQuery(htmlsource) 59 | textarea = dom('div.drop-box textarea') 60 | self.assertEqual(textarea.attr('djng-fileupload-url'), reverse('fileupload')) 61 | self.assertEqual(textarea.attr('ngf-drop'), 'uploadFile($file, "image", "id_avatar", "my_data[\'avatar\']")') 62 | self.assertEqual(textarea.attr('ngf-select'), 'uploadFile($file, "image", "id_avatar", "my_data[\'avatar\']")') 63 | 64 | delete_button = dom('div.drop-box img.djng-btn-trash') 65 | self.assertEqual(delete_button.attr('src'), '/static/djng/icons/image/trash.svg') 66 | self.assertEqual(delete_button.attr('djng-fileupload-button'), '') 67 | self.assertEqual(delete_button.attr('ng-click'), 'deleteImage("id_avatar", "my_data[\'avatar\']")') 68 | 69 | def test_receive_small_image(self): 70 | content = self.upload_image() 71 | content['file:0'].pop('url') 72 | data = {'avatar': content['file:0']} 73 | form = TestUploadForm(data=data) 74 | self.assertTrue(form.is_valid()) 75 | self.assertIsInstance(form.cleaned_data['avatar'], InMemoryUploadedFile) 76 | self.assertEqual(form.cleaned_data['avatar'].name, "sample-image.jpg") 77 | 78 | # TODO: delete this image again 79 | # stored_image = self.storage.save('persisted.jpg', form.cleaned_data['avatar'].file) 80 | # initial = {'avatar': stored_image} 81 | # form = TestUploadForm(initial=initial) 82 | # htmlsource = form.as_p() 83 | 84 | @override_settings(FILE_UPLOAD_MAX_MEMORY_SIZE=50000) 85 | def test_receive_large_image(self): 86 | content = self.upload_image() 87 | content['file:0'].pop('url') 88 | data = {'avatar': content['file:0']} 89 | form = TestUploadForm(data=data) 90 | self.assertTrue(form.is_valid()) 91 | self.assertIsInstance(form.cleaned_data['avatar'], TemporaryUploadedFile) 92 | self.assertEqual(form.cleaned_data['avatar'].name, "sample-image.jpg") 93 | -------------------------------------------------------------------------------- /examples/server/tests/test_postprocessor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django import template 5 | from django.test import TestCase 6 | from sekizai.context import SekizaiContext 7 | 8 | 9 | class PostProcessorTagsTest(TestCase): 10 | 11 | def test_processor_module_list(self): 12 | tpl = template.Template(""" 13 | {% load sekizai_tags %} 14 | {% addtoblock "ng-requires" %}ngAnimate{% endaddtoblock %} 15 | {% render_block "ng-requires" postprocessor "djng.sekizai_processors.module_list" %} 16 | """) 17 | context = SekizaiContext() 18 | output = tpl.render(context) 19 | self.assertIn('ngAnimate', output.strip()) 20 | 21 | def test_processor(self): 22 | tpl = template.Template(""" 23 | {% load sekizai_tags %} 24 | {% addtoblock "ng-config" %}[function() { /* foo */ }]{% endaddtoblock %} 25 | {% render_block "ng-config" postprocessor "djng.sekizai_processors.module_config" %} 26 | """) 27 | context = SekizaiContext() 28 | output = tpl.render(context) 29 | self.assertIn('.config([function() { /* foo */ }])', output.strip()) 30 | -------------------------------------------------------------------------------- /examples/server/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import override_settings, TestCase 5 | from django.test.client import Client 6 | 7 | 8 | TEST_URL_PATH = 'server.tests.urls' 9 | 10 | 11 | @override_settings(ROOT_URLCONF=TEST_URL_PATH) 12 | class TemplateTagsTest(TestCase): 13 | 14 | def test_csrf_token(self): 15 | client = Client(enforce_csrf_checks=True) 16 | response = client.get('/straight_methods/') 17 | self.assertContains(response, '') 12 | context = RequestContext(request, {}) 13 | return HttpResponse(template.render(context)) 14 | 15 | 16 | class TestUrlResolverTagsView(JSONResponseMixin, View): 17 | @allow_remote_invocation 18 | def blah(self, in_data): 19 | return {'blah': 'abc'} 20 | 21 | def get(self, request): 22 | load = request.GET.get('load') 23 | if load == 'root_urls': 24 | template = Template("{% load djng_tags %}{% load_djng_urls None '' %}") 25 | else: 26 | template = Template("{% load djng_tags %}{% load_djng_urls %}") 27 | context = RequestContext(request, {}) 28 | return HttpResponse(template.render(context)) 29 | 30 | 31 | class RemoteMethodsView(JSONResponseMixin, View): 32 | @allow_remote_invocation 33 | def foo(self, in_data): 34 | return {'foo': 'abc'} 35 | 36 | @allow_remote_invocation 37 | def bar(self, in_data): 38 | return {'bar': 'abc'} 39 | 40 | def get(self, request): 41 | template = Template("{% load djng_tags %}{% load_djng_urls 'SELF' %}") 42 | context = RequestContext(request, {}) 43 | return HttpResponse(template.render(context)) 44 | 45 | 46 | subsub_patterns = [ 47 | url(r'^app/$', RemoteMethodsView.as_view(), name='app'), 48 | ] 49 | 50 | sub_patterns = ([ 51 | url(r'^sub/', include(subsub_patterns)), 52 | ], 'sub') 53 | 54 | 55 | class TestAngularTagView(View): 56 | def get(self, request): 57 | tmpl_id = request.GET.get('tmpl_id') 58 | switch = bool(request.GET.get('switch')) 59 | if tmpl_id == 'expand_object': 60 | template = Template('{% load djng_tags %}{% angularjs switch %}{{ expandme.foo }}{% endangularjs %}') 61 | context = {'switch': switch, 'expandme': {'foo': 'bar'}} 62 | elif tmpl_id == 'expand_array': 63 | template = Template('{% load djng_tags %}{% angularjs switch %}{{ expandme.1.foo }}{% endangularjs %}') 64 | context = {'switch': switch, 'expandme': [{'foo': 'zero'}, {'foo': 'one'}]} 65 | else: 66 | template = Template('{% load djng_tags %}{% angularjs switch %}{{ expandme }}{% endangularjs %}') 67 | context = {'switch': switch, 'expandme': 'Hello World'} 68 | request_context = RequestContext(request, context) 69 | return HttpResponse(template.render(request_context)) 70 | 71 | 72 | def locale_script_view(request): 73 | template = Template('{% load djng_tags %}{% djng_locale_script %}') 74 | request_context = RequestContext(request, {}) 75 | return HttpResponse(template.render(request_context)) 76 | 77 | 78 | urlpatterns = [ 79 | url(r'^sub_methods/', include(sub_patterns, namespace='submethods')), 80 | url(r'^straight_methods/$', TestCSRFValueView.as_view(), name='straightmethods'), 81 | url(r'^url_resolvers/$', TestUrlResolverTagsView.as_view(), name='urlresolvertags'), 82 | url(r'^angular_tag/$', TestAngularTagView.as_view(), name='angulartags'), 83 | url(r'^locale_script_tag/$', locale_script_view, name='locale_script'), 84 | ] 85 | -------------------------------------------------------------------------------- /examples/server/tutorial/client-validation.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {% csrf_token %} 7 | {{ form.as_div }} 8 | 9 |
10 | -------------------------------------------------------------------------------- /examples/server/tutorial/combined-validation.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {{ form.as_div }} 10 | 13 | 16 | 19 | 22 |
23 | -------------------------------------------------------------------------------- /examples/server/tutorial/forms-set.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 |
11 |
12 | Subscribe Form 13 | {{ subscribe_form.as_div }} 14 |
15 |
16 | 17 |
18 |
19 | Address Form 20 | {{ address_form.as_div }} 21 |
22 |
23 | 24 | 27 | 28 | 31 | 32 |
-------------------------------------------------------------------------------- /examples/server/tutorial/image-file-upload.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {% csrf_token %} 10 | {{ form.as_div }} 11 | 12 |
13 | -------------------------------------------------------------------------------- /examples/server/tutorial/model-scope.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {% csrf_token %} 10 | {{ form.as_div }} 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /examples/server/tutorial/subscribe-form.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | {% csrf_token %} 7 | {{ form.as_div }} 8 | 9 |
10 | -------------------------------------------------------------------------------- /examples/server/tutorial/three-way-data-binding.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {{ form.as_div }} 10 |
11 | -------------------------------------------------------------------------------- /examples/server/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.conf.urls import url 3 | from django.urls import reverse_lazy 4 | from django.views.generic import RedirectView 5 | from djng.views.upload import FileUploadView 6 | 7 | from server.views.classic_subscribe import SubscribeView as ClassicSubscribeView 8 | from server.views.client_validation import SubscribeView as ClientValidationView 9 | from server.views.model_scope import SubscribeView as ModelScopeView 10 | from server.views.combined_validation import SubscribeView as CombinedValidationView 11 | from server.views.image_file_upload import SubscribeView as ImageAndFileUploadView 12 | from server.views.forms_set import SubscribeView as FormsSetView 13 | from server.views.threeway_databinding import SubscribeView as ThreeWayDataBindingView 14 | from server.views import NgFormDataValidView 15 | 16 | 17 | urlpatterns = [ 18 | url(r'^classic_form/$', ClassicSubscribeView.as_view(), 19 | name='djng_classic_subscription'), 20 | url(r'^form_validation/$', ClientValidationView.as_view(), 21 | name='djng_form_validation'), 22 | url(r'^model_scope/$', ModelScopeView.as_view(), 23 | name='djng_model_scope'), 24 | url(r'^combined_validation/$', CombinedValidationView.as_view(), 25 | name='djng_combined_validation'), 26 | url(r'^image_and_file_upload/$', ImageAndFileUploadView.as_view(), 27 | name='djng_image_file'), 28 | url(r'^form_sets/$', FormsSetView.as_view(), 29 | name='djng_form_sets'), 30 | url(r'^threeway_databinding/$', ThreeWayDataBindingView.as_view(), 31 | name='djng_3way_databinding'), 32 | url(r'^form_data_valid', NgFormDataValidView.as_view(), name='form_data_valid'), 33 | url(r'^upload/$', FileUploadView.as_view(), name='fileupload'), 34 | url(r'^$', RedirectView.as_view(url=reverse_lazy('djng_classic_subscription'))), 35 | ] 36 | -------------------------------------------------------------------------------- /examples/server/views/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.views.generic.base import TemplateView 3 | 4 | 5 | class NgFormDataValidView(TemplateView): 6 | """ 7 | This view just displays a success message, when a valid form was posted successfully. 8 | """ 9 | template_name = 'form-data-valid.html' 10 | -------------------------------------------------------------------------------- /examples/server/views/classic_subscribe.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from server.forms.subscribe_form import SubscribeForm 4 | # start tutorial 5 | from django.views.generic.edit import FormView 6 | from django.urls import reverse_lazy 7 | 8 | 9 | class SubscribeView(FormView): 10 | template_name = 'subscribe-form.html' 11 | form_class = SubscribeForm 12 | success_url = reverse_lazy('form_data_valid') 13 | -------------------------------------------------------------------------------- /examples/server/views/client_validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from server.forms.client_validation import SubscribeForm 3 | # start tutorial 4 | from django.urls import reverse_lazy 5 | from django.views.generic.edit import FormView 6 | 7 | 8 | class SubscribeView(FormView): 9 | template_name = 'client-validation.html' 10 | form_class = SubscribeForm 11 | success_url = reverse_lazy('form_data_valid') 12 | -------------------------------------------------------------------------------- /examples/server/views/combined_validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from server.forms.combined_validation import SubscribeForm, default_subscribe_data 3 | # start tutorial 4 | import json 5 | from django.http import JsonResponse 6 | from django.urls import reverse_lazy 7 | from django.views.generic.edit import FormView 8 | from django.utils.encoding import force_text 9 | 10 | 11 | class SubscribeView(FormView): 12 | template_name = 'combined-validation.html' 13 | form_class = SubscribeForm 14 | success_url = reverse_lazy('form_data_valid') 15 | 16 | def get(self, request, **kwargs): 17 | if request.is_ajax(): 18 | form = self.form_class(initial=default_subscribe_data) 19 | return JsonResponse({form.form_name: form.initial}) 20 | return super(SubscribeView, self).get(request, **kwargs) 21 | 22 | def post(self, request, **kwargs): 23 | assert request.is_ajax() 24 | return self.ajax(request) 25 | 26 | def ajax(self, request): 27 | request_data = json.loads(request.body) 28 | form = self.form_class(data=request_data.get(self.form_class.scope_prefix, {})) 29 | if form.is_valid(): 30 | return JsonResponse({'success_url': force_text(self.success_url)}) 31 | else: 32 | return JsonResponse({form.form_name: form.errors}, status=422) 33 | -------------------------------------------------------------------------------- /examples/server/views/forms_set.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from server.forms.forms_set import SubscribeForm, AddressForm 4 | # start tutorial 5 | import json 6 | from django.http import JsonResponse 7 | from django.views.generic import TemplateView 8 | from django.urls import reverse_lazy 9 | 10 | 11 | class SubscribeView(TemplateView): 12 | template_name = 'forms-set.html' 13 | success_url = reverse_lazy('form_data_valid') 14 | 15 | def get(self, request, *args, **kwargs): 16 | context = self.get_context_data(**kwargs) 17 | context['subscribe_form'] = SubscribeForm() 18 | context['address_form'] = AddressForm() 19 | return self.render_to_response(context) 20 | 21 | def put(self, request, *args, **kwargs): 22 | request_data = json.loads(request.body) 23 | subscribe_form = SubscribeForm(data=request_data.get(SubscribeForm.scope_prefix, {})) 24 | address_form = AddressForm(data=request_data.get(AddressForm.scope_prefix, {})) 25 | response_data = {} 26 | 27 | if subscribe_form.is_valid() and address_form.is_valid(): 28 | response_data.update({'success_url': self.success_url}) 29 | return JsonResponse(response_data) 30 | 31 | # otherwise report form validation errors 32 | response_data.update({ 33 | subscribe_form.form_name: subscribe_form.errors, 34 | address_form.form_name: address_form.errors, 35 | }) 36 | return JsonResponse(response_data, status=422) 37 | -------------------------------------------------------------------------------- /examples/server/views/image_file_upload.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from server.forms.image_file_upload import SubscribeForm 3 | # from server.models.image_file_upload import SubscribeForm 4 | # start tutorial 5 | import json 6 | from django.http import JsonResponse 7 | from django.urls import reverse_lazy 8 | from django.utils.encoding import force_text 9 | from django.views.generic.edit import FormView 10 | 11 | 12 | class SubscribeView(FormView): 13 | template_name = 'image-file-upload.html' 14 | form_class = SubscribeForm 15 | success_url = reverse_lazy('form_data_valid') 16 | 17 | def post(self, request, **kwargs): 18 | assert request.is_ajax() 19 | return self.ajax(request) 20 | 21 | def ajax(self, request): 22 | request_data = json.loads(request.body) 23 | form = self.form_class(data=request_data.get(self.form_class.scope_prefix, {})) 24 | if form.is_valid(): 25 | return JsonResponse({'success_url': force_text(self.success_url)}) 26 | else: 27 | return JsonResponse({form.form_name: form.errors}, status=422) 28 | -------------------------------------------------------------------------------- /examples/server/views/model_scope.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from server.forms.model_scope import SubscribeForm 3 | # start tutorial 4 | import json 5 | from django.http import JsonResponse 6 | from django.urls import reverse_lazy 7 | from django.utils.encoding import force_text 8 | from django.views.generic.edit import FormView 9 | 10 | 11 | class SubscribeView(FormView): 12 | template_name = 'model-scope.html' 13 | form_class = SubscribeForm 14 | success_url = reverse_lazy('form_data_valid') 15 | 16 | def post(self, request, **kwargs): 17 | if request.is_ajax(): 18 | return self.ajax(request) 19 | return super(SubscribeView, self).post(request, **kwargs) 20 | 21 | def ajax(self, request): 22 | request_data = json.loads(request.body) 23 | form = self.form_class(data=request_data.get(self.form_class.scope_prefix, {})) 24 | if form.is_valid(): 25 | return JsonResponse({'success_url': force_text(self.success_url)}) 26 | else: 27 | return JsonResponse({form.form_name: form.errors}, status=422) 28 | -------------------------------------------------------------------------------- /examples/server/views/threeway_databinding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from server.forms.model_scope import SubscribeForm 3 | # start tutorial 4 | from django.views.generic.edit import FormView 5 | 6 | 7 | class SubscribeView(FormView): 8 | template_name = 'three-way-data-binding.html' 9 | form_class = SubscribeForm 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | from djng import __version__ 4 | 5 | with open('README.md', 'r') as fh: 6 | long_description = fh.read() 7 | 8 | DESCRIPTION = 'Let Django play well with AngularJS' 9 | 10 | CLASSIFIERS = [ 11 | 'Environment :: Web Environment', 12 | 'Framework :: Django', 13 | 'Intended Audience :: Developers', 14 | 'License :: OSI Approved :: MIT License', 15 | 'Operating System :: OS Independent', 16 | 'Programming Language :: Python', 17 | 'Topic :: Software Development :: Libraries :: Python Modules', 18 | 'Development Status :: 5 - Production/Stable', 19 | 'Programming Language :: Python :: 3.5', 20 | 'Programming Language :: Python :: 3.6', 21 | 'Programming Language :: Python :: 3.7', 22 | 'Programming Language :: Python :: 3.8', 23 | 'Framework :: Django :: 2.1', 24 | 'Framework :: Django :: 2.2', 25 | 'Framework :: Django :: 3.0', 26 | 'Framework :: Django :: 3.1', 27 | ] 28 | 29 | setup( 30 | name='django-angular', 31 | version=__version__, 32 | author='Jacob Rief', 33 | author_email='jacob.rief@gmail.com', 34 | description=DESCRIPTION, 35 | install_requires=['django>=2.1'], 36 | long_description=long_description, 37 | long_description_content_type='text/markdown', 38 | url='https://github.com/jrief/django-angular', 39 | license='MIT', 40 | keywords=['Django', 'AngularJS'], 41 | platforms=['OS Independent'], 42 | classifiers=CLASSIFIERS, 43 | packages=find_packages(exclude=['examples', 'docs']), 44 | include_package_data=True, 45 | ) 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = coverage-clean, py{36,37,38}-django{21,22,30,31}, coverage-report 3 | skipsdist = true 4 | 5 | [testenv] 6 | commands = 7 | pip install -e . 8 | coverage run -a {envbindir}/py.test examples 9 | whitelist_externals = 10 | coverage 11 | deps= 12 | cssselect==1.1.0 13 | pyquery==1.4.0 14 | pygments==2.1.3 15 | pytest==4.6.6 16 | pytest-django==3.5.1 17 | tox==3.13.2 18 | beautifulsoup4==4.8.1 19 | coverage==4.5.4 20 | easy-thumbnails 21 | django-classy-tags 22 | django-sekizai 23 | django21: Django<2.2 24 | django22: Django<3.0 25 | django30: Django<3.1 26 | django31: Django<3.2 27 | 28 | [testenv:coverage-clean] 29 | commands = rm -f .coverage 30 | deps= 31 | whitelist_externals = 32 | /bin/rm 33 | coverage 34 | 35 | [testenv:coverage-report] 36 | commands = coverage report 37 | deps= 38 | whitelist_externals = 39 | coverage 40 | 41 | [testenv:client-tests] 42 | commands = 43 | cd client 44 | npm install 45 | npm run test-single-run 46 | --------------------------------------------------------------------------------