├── .codecov.yml ├── .coveragerc ├── .gitattributes ├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── intl_tel_input ├── __init__.py ├── static │ └── intl_tel_input │ │ └── init.js └── widgets.py ├── setup.cfg ├── setup.py ├── testapp ├── __init__.py ├── forms.py ├── manage.py ├── settings.py ├── templates │ └── home.html ├── tests.py ├── urls.py ├── views.py └── wsgi.py └── tox.ini /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "testapp/wsgi.py" 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | ignore_errors = True 6 | omit = 7 | *tests* 8 | testapp/manage.py 9 | testapp/wsgi.py 10 | 11 | [html] 12 | directory = coverage_html_report 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .vscode 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | # Python build and distribution 51 | build 52 | dist 53 | *.egg-info 54 | 55 | # Testing 56 | .venv 57 | .coverage 58 | .tox 59 | *.pyc 60 | testapp/db.sqlite3 61 | coverage_html_report 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.5" 5 | - "3.6" 6 | 7 | sudo: false 8 | 9 | addons: 10 | firefox: latest 11 | 12 | before_install: 13 | - wget https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz 14 | - mkdir geckodriver 15 | - tar -xzf geckodriver-v0.21.0-linux64.tar.gz -C geckodriver 16 | - export PATH=$PATH:$PWD/geckodriver 17 | 18 | env: 19 | - DJANGO=1.11 20 | - DJANGO=2.0 21 | - DJANGO=2.1 22 | 23 | matrix: 24 | fast_finish: true 25 | include: 26 | - python: "3.7" 27 | dist: xenial 28 | env: DJANGO=1.11 29 | - python: "3.7" 30 | dist: xenial 31 | env: DJANGO=2.0 32 | - python: "3.7" 33 | dist: xenial 34 | env: DJANGO=2.1 35 | 36 | - python: "3.4" 37 | env: DJANGO=1.11 38 | - python: "3.4" 39 | env: DJANGO=2.0 40 | 41 | - python: "2.7" 42 | env: DJANGO=1.11 43 | 44 | - python: "3.7" 45 | dist: xenial 46 | env: TOXENV="flake8" 47 | - python: "3.7" 48 | dist: xenial 49 | env: TOXENV="isort" 50 | 51 | install: 52 | - pip install tox tox-travis 53 | 54 | script: 55 | - tox 56 | 57 | after_success: 58 | - pip install codecov 59 | - codecov -e TOXENV,DJANGO 60 | 61 | notifications: 62 | email: false 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Benjamin Murden 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of contributors may not be used to endorse or promote products 12 | derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL BENJAMIN MURDEN BE LIABLE FOR ANY 18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include intl_tel_input * 2 | include README.md 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django intl-tel-input 2 | ===================== 3 | 4 | [![Build Status](https://travis-ci.org/benmurden/django-intl-tel-input.svg?branch=master)](https://travis-ci.org/benmurden/django-intl-tel-input) 5 | [![Code coverage](https://img.shields.io/codecov/c/github/benmurden/django-intl-tel-input.svg)](https://codecov.io/gh/benmurden/django-intl-tel-input) 6 | 7 | A Django form widget for international telephone numbers based on the 8 | jQuery plugin [intl-tel-input](https://github.com/jackocnr/intl-tel-input). 9 | 10 | This package is pre 1.0, so doesn't implement every feature of intl-tel-input. However, it is well tested, and has been stable in production. 11 | 12 | ## Version support 13 | 14 | Tested on the following versions of Python and Django. 15 | 16 | | Package | Version support | 17 | | ------- | ----------------------------- | 18 | | Python | 2.7, 3.4, 3.5, 3.6, 3.7, 3.10 | 19 | | Django | 1.11, 2.0, 2.1, 3.0, 4.0 | 20 | 21 | ## Installation 22 | 23 | Install from PyPI. 24 | 25 | ```shell 26 | pip install django-intl-tel-input 27 | ``` 28 | 29 | Add intl-tel-input to your `INSTALLED_APPS`, so Django can find the init 30 | script. 31 | 32 | ```python 33 | ... 34 | INSTALLED_APPS += ('intl_tel_input',) 35 | ... 36 | ``` 37 | 38 | ## Usage 39 | 40 | Simply add `IntlTelInputWidget` to your form field. 41 | 42 | ```python 43 | from intl_tel_input.widgets import IntlTelInputWidget 44 | 45 | class MyForm(forms.ModelForm): 46 | class Meta: 47 | model = MyModel 48 | fields = ['foo', 'bar'] 49 | widgets = { 50 | 'bar': IntlTelInputWidget() 51 | } 52 | ... 53 | ``` 54 | 55 | With a standard form: 56 | 57 | ```python 58 | class MyForm(forms.Form): 59 | tel_number = forms.CharField(widget=IntlTelInputWidget()) 60 | 61 | ... 62 | ``` 63 | 64 | ## Form media 65 | 66 | Include `{{ form.media.css }}` in the `` of your template. This 67 | will ensure all styles are parsed before the widget is displayed. 68 | 69 | If you have included jQuery at the end of your document, then don't 70 | forget to update the template where this widget appears with a 71 | `{{ form.media.js }}`. Put it in a block that allows it to come after 72 | jQuery. 73 | 74 | If you're using 75 | [crispy-forms](https://github.com/django-crispy-forms/django-crispy-forms), 76 | the static content will be inserted automatically beside the input. To 77 | prevent this, be sure to set `include_media = False` on your form 78 | helper. 79 | 80 | ```python 81 | class MyForm(forms.Form): 82 | ... 83 | def __init__(self, *args, **kwargs): 84 | self.helper = FormHelper() 85 | self.helper.include_media = False 86 | ... 87 | ``` 88 | 89 | If you need to load all JS in the head, you can make the `init.js` 90 | script wait for the document to be ready with the following snippet. 91 | 92 | ```javascript 93 | jQuery(document).ready( 94 | {{ form.media.js }} 95 | ); 96 | ``` 97 | 98 | All this assumes your form context variable is called `form`. 99 | 100 | ## Options 101 | 102 | The widget can be invoked with keyword arguments which translate to the 103 | options available in intl-tel-input. 104 | 105 | ### allow\_dropdown 106 | Shows the country dropdown. 107 | Default: `True` 108 | 109 | ### default\_code 110 | Country code selected by default. Overridden when using `auto_geo_ip`. 111 | Default: `'us'` 112 | 113 | ### preferred\_countries 114 | Array of countries that will always appear at the top of the dropdown. 115 | Default: `['us', 'gb']` 116 | 117 | ### use\_default\_init 118 | Use the provided init.js to initialize the plugin. Set this to `False` 119 | if you want to provide your own initialization for the plugin. This is 120 | useful if, for example, you have your own GeoIP implementation you'd 121 | like to use. 122 | Default: `True` 123 | -------------------------------------------------------------------------------- /intl_tel_input/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benmurden/django-intl-tel-input/f878bc35c893124c49d771aafbbe8d2c663ac2ce/intl_tel_input/__init__.py -------------------------------------------------------------------------------- /intl_tel_input/static/intl_tel_input/init.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | var options, data, 3 | cssClass = '.intl-tel-input', 4 | inputs = $(cssClass); 5 | 6 | inputs.each(function(i, el) { 7 | var $el; 8 | 9 | $el = $(el); 10 | data = $el.data(); 11 | options = { 12 | initialCountry: data.defaultCode, 13 | allowDropdown: data.allowDropdown !== undefined ? true : false, 14 | hiddenInput: data.hiddenName 15 | }; 16 | 17 | options.utilsScript = 'https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.19/js/utils.js'; 18 | options.preferredCountries = data.preferredCountries; 19 | 20 | $el.intlTelInput(options); 21 | }); 22 | })(jQuery); 23 | -------------------------------------------------------------------------------- /intl_tel_input/widgets.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import forms 4 | from django.forms.utils import flatatt 5 | try: 6 | from django.utils.encoding import force_str as force_text 7 | except ImportError: 8 | from django.utils.encoding import force_text 9 | from django.utils.html import format_html 10 | from django.utils.safestring import mark_safe 11 | 12 | INTL_TEL_INPUT_VERSION = '17.0.19' 13 | 14 | 15 | class IntlTelInputWidget(forms.TextInput): 16 | input_type = 'tel' 17 | 18 | def __init__(self, attrs=None, allow_dropdown=True, 19 | preferred_countries=['us', 'gb'], default_code='us', 20 | use_default_init=True): 21 | 22 | self.use_default_init = use_default_init 23 | 24 | final_attrs = { 25 | 'size': '20', 26 | 'data-allow-dropdown': allow_dropdown, 27 | 'data-preferred-countries': json.dumps(preferred_countries), 28 | 'data-default-code': default_code, 29 | } 30 | 31 | if attrs is not None: 32 | final_attrs.update(attrs) 33 | 34 | super(IntlTelInputWidget, self).__init__(attrs=final_attrs) 35 | 36 | @property 37 | def media(self): 38 | js = ( 39 | 'https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/' 40 | '{version}/js/intlTelInput-jquery.min.js'.format( 41 | version=INTL_TEL_INPUT_VERSION 42 | ), 43 | ) 44 | 45 | if self.use_default_init: 46 | js += ('intl_tel_input/init.js',) 47 | 48 | return forms.Media( 49 | css={ 50 | 'all': ( 51 | 'https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/' 52 | '{version}/css/intlTelInput.css'.format( 53 | version=INTL_TEL_INPUT_VERSION 54 | ), 55 | ), 56 | }, 57 | js=js 58 | ) 59 | 60 | def render(self, name, value, renderer=None, attrs=None): 61 | if value is None: 62 | value = '' 63 | 64 | final_attrs = self.build_attrs(attrs, self.attrs) 65 | final_attrs['data-hidden-name'] = name 66 | 67 | if value != '': 68 | final_attrs['value'] = force_text(self.format_value(value)) 69 | 70 | final_attrs['class'] = ' '.join([ 71 | 'intl-tel-input', final_attrs.get('class', '') 72 | ]).strip() 73 | 74 | output = format_html('', flatatt(final_attrs)) 75 | return mark_safe(output) 76 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 6 | README = readme.read() 7 | 8 | # allow setup.py to be run from any path 9 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 10 | 11 | setup( 12 | name='django-intl-tel-input', 13 | version='0.4.0', 14 | packages=find_packages(), 15 | include_package_data=True, 16 | license='BSD License', 17 | description='A Django form widget implementing intl-tel-input.', 18 | long_description=README, 19 | long_description_content_type="text/markdown", 20 | url='https://github.com/benmurden/django-intl-tel-input', 21 | author='Benjamin Murden', 22 | author_email='benmurden@github.com', 23 | classifiers=[ 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Framework :: Django :: 1.11', 27 | 'Framework :: Django :: 2.0', 28 | 'Framework :: Django :: 3.0', 29 | 'Framework :: Django :: 4.0', 30 | 'Framework :: Django :: 5.0', 31 | 'Intended Audience :: Developers', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Operating System :: OS Independent', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | 'Programming Language :: Python :: 3.6', 40 | 'Programming Language :: Python :: 3.7', 41 | 'Topic :: Internet :: WWW/HTTP', 42 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 43 | ], 44 | install_requires=['Django>=1.11'] 45 | ) 46 | -------------------------------------------------------------------------------- /testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benmurden/django-intl-tel-input/f878bc35c893124c49d771aafbbe8d2c663ac2ce/testapp/__init__.py -------------------------------------------------------------------------------- /testapp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from intl_tel_input.widgets import IntlTelInputWidget 4 | 5 | 6 | class TelForm(forms.Form): 7 | tel_number = forms.CharField(widget=IntlTelInputWidget(), required=True) 8 | 9 | 10 | class TelFormAttrs(forms.Form): 11 | tel_number = forms.CharField(widget=IntlTelInputWidget( 12 | attrs={'title': 'Telephone number', 'placeholder': 'foobar'}, 13 | preferred_countries=['jp'], 14 | default_code='jp' 15 | )) 16 | 17 | 18 | class TwoTelForm(TelForm): 19 | tel_number_2 = forms.CharField(widget=IntlTelInputWidget(), required=True) 20 | 21 | 22 | class TelFormNoInit(forms.Form): 23 | tel_number = forms.CharField(widget=IntlTelInputWidget( 24 | use_default_init=False 25 | )) 26 | -------------------------------------------------------------------------------- /testapp/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", "settings") 7 | 8 | BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 9 | sys.path.insert(0, BASEDIR) 10 | 11 | from django.core.management import execute_from_command_line 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /testapp/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 4 | 5 | SECRET_KEY = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 6 | 7 | DEBUG = True 8 | 9 | INTERNAL_IPS = ['127.0.0.1'] 10 | 11 | LOGGING_CONFIG = None 12 | 13 | INSTALLED_APPS = ( 14 | 'django.contrib.admin', 15 | 'django.contrib.auth', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.sessions', 18 | 'django.contrib.messages', 19 | 'django.contrib.staticfiles', 20 | 'intl_tel_input', 21 | ) 22 | 23 | MIDDLEWARE_CLASSES = ( 24 | 'django.middleware.security.SecurityMiddleware', 25 | 'django.contrib.sessions.middleware.SessionMiddleware', 26 | 'django.middleware.common.CommonMiddleware', 27 | 'django.middleware.csrf.CsrfViewMiddleware', 28 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 29 | 'django.contrib.messages.middleware.MessageMiddleware', 30 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 31 | ) 32 | 33 | ROOT_URLCONF = 'testapp.urls' 34 | 35 | TEMPLATES = [ 36 | { 37 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 38 | 'DIRS': ['testapp/templates'], 39 | 'APP_DIRS': True, 40 | 'OPTIONS': { 41 | 'debug': True, 42 | 'context_processors': [ 43 | 'django.template.context_processors.debug', 44 | 'django.template.context_processors.request', 45 | 'django.contrib.auth.context_processors.auth', 46 | 'django.contrib.messages.context_processors.messages', 47 | ], 48 | }, 49 | }, 50 | ] 51 | 52 | DATABASES = { 53 | 'default': { 54 | 'ENGINE': 'django.db.backends.sqlite3', 55 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 56 | } 57 | } 58 | 59 | STATIC_URL = '/static/' 60 | -------------------------------------------------------------------------------- /testapp/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | django-intl-tel-input test 5 | {{ form.media.css }} 6 | 7 | 8 |
9 | {% if form.is_valid %} 10 |

Form is valid

11 | {% endif %} 12 | {% csrf_token %} 13 | {{ form.as_p }} 14 | 15 |
16 | 17 | {{ form.media.js }} 18 | 19 | 20 | -------------------------------------------------------------------------------- /testapp/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import html5lib 4 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 5 | from django.test import TestCase 6 | 7 | from selenium.webdriver import Firefox 8 | from selenium.webdriver.common.by import By 9 | from selenium.webdriver.common.keys import Keys 10 | from selenium.webdriver.firefox.options import Options 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | 14 | 15 | class IntlTelInputTest(TestCase): 16 | def assertValidHTML(self, content, msg=None): 17 | parser = html5lib.HTMLParser() 18 | parser.parse(content) 19 | if parser.errors: 20 | default_msg = ['Content is invalid HTML:'] 21 | lines = content.split('\n') 22 | for position, errorcode, datavars in parser.errors: 23 | default_msg.append( 24 | ' %s' % html5lib.constants.E[errorcode] % datavars 25 | ) 26 | default_msg.append(' %s' % lines[position[0] - 1]) 27 | 28 | msg = self._formatMessage(msg, '\n'.join(default_msg)) 29 | raise self.failureException(msg) 30 | 31 | def test_render(self): 32 | r = self.client.get('/') 33 | self.assertValidHTML(r.content) 34 | self.assertEqual(r.status_code, 200) 35 | 36 | def test_static(self): 37 | r = self.client.get('/') 38 | self.assertIn( 39 | '/js/intlTelInput-jquery.min.js', 40 | r.content.decode('utf-8') 41 | ) 42 | self.assertIn('/intl_tel_input/init.js', r.content.decode('utf-8')) 43 | self.assertIn('/css/intlTelInput.css', r.content.decode('utf-8')) 44 | 45 | def test_post(self): 46 | r = self.client.post('/', {'tel_number': '+81123456789'}) 47 | self.assertRedirects(r, '/?ok') 48 | 49 | def test_with_attrs(self): 50 | r = self.client.get('/attrs-test/') 51 | self.assertIn('title="Telephone number"', r.content.decode('utf-8')) 52 | self.assertIn('data-default-code="jp"', r.content.decode('utf-8')) 53 | self.assertIn('data-preferred-countries="["jp"]"', 54 | r.content.decode('utf-8')) 55 | self.assertIn('placeholder="foobar"', r.content.decode('utf-8')) 56 | 57 | def test_with_initial(self): 58 | r = self.client.get('/initial-test/') 59 | self.assertIn('value="+81123456789"', r.content.decode('utf-8')) 60 | 61 | def test_use_default_init(self): 62 | r = self.client.get('/no-init-test/') 63 | self.assertNotIn('/intl_tel_input/init.js', r.content.decode('utf-8')) 64 | 65 | 66 | class wait_for_utils_script(object): 67 | def __call__(self, driver): 68 | return driver.execute_script( 69 | "return window.intlTelInputUtils !== undefined" 70 | ) 71 | 72 | 73 | class AcceptanceTest(StaticLiveServerTestCase): 74 | def setUp(self): 75 | options = Options() 76 | options.headless = True 77 | self.driver = Firefox(options=options) 78 | 79 | def tearDown(self): 80 | self.driver.quit() 81 | 82 | def test_two_inputs(self): 83 | driver = self.driver 84 | driver.get('{live_server_url}/two-field-test/'.format( 85 | live_server_url=self.live_server_url 86 | )) 87 | WebDriverWait(driver, 5).until( 88 | wait_for_utils_script() 89 | ) 90 | inputs = driver.find_elements_by_css_selector('input.intl-tel-input') 91 | inputs[0].send_keys('555-5555') 92 | inputs[1].send_keys('555-4444') 93 | inputs[1].send_keys(Keys.RETURN) 94 | WebDriverWait(driver, 5).until( 95 | EC.presence_of_element_located((By.ID, 'success-text')) 96 | ) 97 | 98 | self.assertIn('Form is valid', driver.page_source) 99 | -------------------------------------------------------------------------------- /testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import (attrs_test, home, initial_test, no_init_test, 4 | two_fields_test) 5 | 6 | urlpatterns = [ 7 | url(r'^$', home), 8 | url(r'^attrs-test/$', attrs_test), 9 | url(r'^initial-test/$', initial_test), 10 | url(r'^two-field-test/$', two_fields_test), 11 | url(r'^no-init-test/$', no_init_test) 12 | ] 13 | -------------------------------------------------------------------------------- /testapp/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.shortcuts import render 3 | 4 | from .forms import TelForm, TelFormAttrs, TelFormNoInit, TwoTelForm 5 | 6 | 7 | def home(request): 8 | if request.POST: 9 | form = TelForm(request.POST) 10 | return HttpResponseRedirect('{path}?ok'.format(path=request.path)) 11 | else: 12 | form = TelForm() 13 | 14 | return render(request, 'home.html', {'form': form}) 15 | 16 | 17 | def attrs_test(request): 18 | form = TelFormAttrs() 19 | return render(request, 'home.html', {'form': form}) 20 | 21 | 22 | def initial_test(request): 23 | form = TelForm(initial={'tel_number': '+81123456789'}) 24 | return render(request, 'home.html', {'form': form}) 25 | 26 | 27 | def two_fields_test(request): 28 | if request.POST: 29 | form = TwoTelForm(request.POST) 30 | else: 31 | form = TwoTelForm() 32 | return render(request, 'home.html', {'form': form}) 33 | 34 | 35 | def no_init_test(request): 36 | form = TelFormNoInit() 37 | return render(request, 'home.html', {'form': form}) 38 | -------------------------------------------------------------------------------- /testapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py27,py34,py35,py36,py37}-django111, 4 | {py34,py35,py36,py37}-django20, 5 | {py35,py36,py37}-django21, 6 | flake8,isort 7 | 8 | skipsdist = True 9 | 10 | 11 | [isort] 12 | skip=.tox 13 | known_third_party=django,html5lib 14 | known_first_party=intl_tel_input 15 | 16 | 17 | [travis:env] 18 | DJANGO = 19 | 1.11: django111 20 | 2.0: django20 21 | 2.1: django21 22 | 23 | 24 | [testenv] 25 | commands = 26 | coverage run testapp/manage.py test testapp --failfast 27 | 28 | setenv = 29 | PYTHONDONTWRITEBYTECODE=1 30 | PYTHONPATH = {toxinidir} 31 | 32 | deps = 33 | django111: Django>=1.11,<2.0 34 | django20: Django>=2.0,<2.1 35 | django21: Django>=2.1,<2.2 36 | coverage 37 | html5lib 38 | selenium 39 | 40 | 41 | [testenv:flake8] 42 | basepython = python3.7 43 | deps = 44 | flake8 45 | commands= 46 | flake8 intl_tel_input testapp 47 | 48 | [testenv:isort] 49 | basepython = python3.7 50 | deps = 51 | isort 52 | commands= 53 | isort -rc -c intl_tel_input testapp 54 | --------------------------------------------------------------------------------