├── .flake8 ├── .github ├── FUNDING.yml └── workflows │ ├── publish.yml │ ├── test-pr.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── address ├── __init__.py ├── admin.py ├── apps.py ├── compat.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160213_1726.py │ ├── 0003_auto_20200830_1851.py │ └── __init__.py ├── models.py ├── static │ ├── address │ │ └── js │ │ │ └── address.js │ └── js │ │ ├── jquery.geocomplete.js │ │ └── jquery.geocomplete.min.js ├── tests │ ├── __init__.py │ ├── test_forms.py │ └── test_models.py └── widgets.py ├── docker-compose.yml ├── example_site ├── .gitignore ├── README.md ├── example_site │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── person │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20200628_1720.py │ │ ├── 0003_auto_20200628_1920.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── example │ │ │ └── home.html │ └── views.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt └── tox.ini ├── poetry.lock ├── pyproject.toml ├── setup.cfg └── setup.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 119 3 | exclude = */migrations/*,.tox 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [banagale] -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Test and publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - '**.py' 8 | - 'example_site/pyproject.toml' 9 | - 'example_site/poetry.lock' 10 | - 'example_site/tox.ini' 11 | - '.github/workflows/publish.yml' 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python: [3.6, 3.9] 20 | services: 21 | postgres: 22 | image: postgres 23 | env: 24 | POSTGRES_PASSWORD: postgres 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | ports: 31 | - 5432:5432 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Setup Python 38 | uses: actions/setup-python@v2.1.2 39 | env: 40 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 41 | 42 | - name: Setup Poetry 43 | uses: Gr1N/setup-poetry@v3 44 | env: 45 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 46 | 47 | - name: Install Tox 48 | run: pip install tox 49 | 50 | - name: Run tests 51 | env: 52 | DATABASE_URL: postgres://postgres:postgres@localhost/postgres 53 | run: | 54 | cd example_site 55 | cp -r ../address . 56 | tox -e py 57 | 58 | lint: 59 | name: Lint 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v2 64 | 65 | - name: Setup Poetry 66 | uses: Gr1N/setup-poetry@v3 67 | env: 68 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 69 | 70 | - name: Install dependencies 71 | run: | 72 | poetry install 73 | 74 | - name: Run flake8 75 | run: poetry run flake8 76 | 77 | - name: Run Black 78 | run: poetry run black --check . 79 | 80 | bump: 81 | name: Bump version 82 | runs-on: ubuntu-latest 83 | needs: [test, lint] 84 | steps: 85 | - name: Checkout 86 | uses: actions/checkout@v2 87 | 88 | - name: Bump 89 | run: echo TODO 90 | 91 | publish: 92 | name: Publish 93 | runs-on: ubuntu-latest 94 | needs: [bump] 95 | steps: 96 | - name: Checkout 97 | uses: actions/checkout@v2 98 | 99 | - name: Publish 100 | run: echo TODO 101 | -------------------------------------------------------------------------------- /.github/workflows/test-pr.yml: -------------------------------------------------------------------------------- 1 | name: Test PR 2 | on: 3 | pull_request: 4 | branches: 5 | - '*' 6 | paths: 7 | - '**.py' 8 | - 'example_site/pyproject.toml' 9 | - 'example_site/poetry.lock' 10 | - 'example_site/tox.ini' 11 | - '.github/workflows/test-pr.yml' 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python: [3.6, 3.9] 20 | services: 21 | postgres: 22 | image: postgres 23 | env: 24 | POSTGRES_PASSWORD: postgres 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | ports: 31 | - 5432:5432 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Setup Python 38 | uses: actions/setup-python@v2.1.2 39 | with: 40 | python-version: ${{ matrix.python }} 41 | env: 42 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 43 | 44 | - name: Setup Poetry 45 | uses: Gr1N/setup-poetry@v3 46 | env: 47 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 48 | 49 | - name: Install Tox 50 | run: pip install tox 51 | 52 | - name: Run tests 53 | env: 54 | DATABASE_URL: postgres://postgres:postgres@localhost/postgres 55 | run: | 56 | cd example_site 57 | cp -r ../address . 58 | tox -e py 59 | 60 | lint: 61 | name: Lint 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v2 66 | 67 | - name: Setup Python 68 | uses: actions/setup-python@v2.1.2 69 | env: 70 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 71 | 72 | - name: Setup Poetry 73 | uses: Gr1N/setup-poetry@v3 74 | env: 75 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 76 | 77 | - name: Install dependencies 78 | run: | 79 | poetry install 80 | 81 | - name: Run flake8 82 | run: poetry run flake8 83 | 84 | - name: Run Black 85 | run: poetry run black --check . 86 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | paths: 7 | - '**.py' 8 | - 'example_site/pyproject.toml' 9 | - 'example_site/poetry.lock' 10 | - 'example_site/tox.ini' 11 | - '.github/workflows/test.yml' 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python: [3.6, 3.9] 20 | services: 21 | postgres: 22 | image: postgres 23 | env: 24 | POSTGRES_PASSWORD: postgres 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | ports: 31 | - 5432:5432 32 | 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | 37 | - name: Setup Python 38 | uses: actions/setup-python@v2.1.2 39 | env: 40 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 41 | 42 | - name: Setup Poetry 43 | uses: Gr1N/setup-poetry@v3 44 | env: 45 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 46 | 47 | - name: Install Tox 48 | run: pip install tox 49 | 50 | - name: Run tests 51 | env: 52 | DATABASE_URL: postgres://postgres:postgres@localhost/postgres 53 | run: | 54 | cd example_site 55 | cp -r ../address . 56 | tox -e py 57 | 58 | lint: 59 | name: Lint 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v2 64 | 65 | - name: Setup Poetry 66 | uses: Gr1N/setup-poetry@v3 67 | env: 68 | ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' 69 | 70 | - name: Install dependencies 71 | run: | 72 | poetry install 73 | 74 | - name: Run flake8 75 | run: poetry run flake8 76 | 77 | - name: Run Black 78 | run: poetry run black --check . 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | secret_const.py 5 | build/** 6 | 7 | __pycache__/ 8 | local_settings.py 9 | db.sqlite3 10 | media 11 | dist/ 12 | *.egg-info/ 13 | .idea 14 | .DS_Store 15 | 16 | .envrc 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | LABEL maintainer="furious.luke@gmail.com" 3 | 4 | ENV PYTHONDONTWRITEBYTECODE=1 \ 5 | PYTHONUNBUFFERED=1 \ 6 | PYTHONIOENCODING=utf-8 \ 7 | LANG=C.UTF-8 8 | 9 | RUN apt-get -qq update \ 10 | && apt-get -y install \ 11 | bash \ 12 | locales \ 13 | git \ 14 | build-essential \ 15 | libssl-dev \ 16 | && pip install poetry \ 17 | && rm -rf /var/lib/apt/lists/* \ 18 | && ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime \ 19 | && locale-gen C.UTF-8 || true 20 | 21 | ARG USER_ID 22 | ARG GROUP_ID 23 | RUN addgroup --gid $GROUP_ID user || true \ 24 | && useradd -M -u $USER_ID -g $GROUP_ID user || true \ 25 | && usermod -d /code user || true 26 | 27 | RUN mkdir -p /code 28 | WORKDIR /code 29 | 30 | COPY ./example_site/pyproject.toml ./example_site/poetry.lock /code/ 31 | RUN poetry config virtualenvs.create false \ 32 | && poetry install --no-interaction --no-ansi 33 | 34 | COPY ./example_site /code/ 35 | COPY ./address /code/address 36 | RUN chown -R user:user /code 37 | USER user 38 | 39 | EXPOSE 8000 40 | 41 | CMD ./manage.py migrate && ./manage.py runserver 0.0.0.0:8000 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Luke Hodkinson 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 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include address/static * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Address 2 | 3 | **Django models for storing and retrieving postal addresses.** 4 | 5 | --- 6 | 7 | # Overview 8 | Django Address is a set of models and methods for working with postal addresses. 9 | 10 | # Requirements 11 | * Python (3.5, 3.6, 3.7, 3.8) 12 | * Django (2.2, 3.0) 13 | 14 | We **recommend** and only officially support the latest patch release of each Python and Django series. 15 | 16 | # Installation 17 | For more detailed instructions, [view the Readme for the example site](https://github.com/furious-luke/django-address/blob/master/example_site/README.md) included with this package. 18 | 19 | ```bash 20 | pip install django-address 21 | ``` 22 | 23 | Then, add `address` to your `INSTALLED_APPS` list in `settings.py`: 24 | 25 | ```python 26 | INSTALLED_APPS = [ 27 | # ... 28 | 'address', 29 | # ... 30 | ] 31 | ``` 32 | 33 | You can either store your Google API key in an environment variable as `GOOGLE_API_KEY` or you can 34 | specify the key in `settings.py`. If you have an environment variable set it will override what you put in settings.py. 35 | For more information, including enabling the Google Places API, refer to [the example site](https://github.com/furious-luke/django-address/blob/master/example_site/README.md). 36 | 37 | ``` 38 | GOOGLE_API_KEY = 'AIzaSyD--your-google-maps-key-SjQBE' 39 | ``` 40 | 41 | # The Model 42 | 43 | The rationale behind the model structure is centered on trying to make 44 | it easy to enter addresses that may be poorly defined. The model field included 45 | uses Google Maps API v3 (via the nicely done [geocomplete jquery plugin](http://ubilabs.github.io/geocomplete/)) to 46 | determine a proper address where possible. However if this isn't possible the 47 | raw address is used and the user is responsible for breaking the address down 48 | into components. 49 | 50 | It's currently assumed any address is represent-able using four components: 51 | country, state, locality and street address. In addition, country code, state 52 | code and postal code may be stored, if they exist. 53 | 54 | There are four Django models used: 55 | 56 | ``` 57 | Country 58 | name 59 | code 60 | 61 | State 62 | name 63 | code 64 | country -> Country 65 | 66 | Locality 67 | name 68 | postal_code 69 | state -> State 70 | 71 | Address 72 | raw 73 | street_number 74 | route 75 | locality -> Locality 76 | ``` 77 | 78 | # Address Field 79 | 80 | To simplify storage and access of addresses, a subclass of `ForeignKey` named 81 | `AddressField` has been created. It provides an easy method for setting new 82 | addresses. 83 | 84 | ## ON_DELETE behavior of Address Field 85 | 86 | By default, if you delete an Address that is related to another object, 87 | Django's [cascade behavior](https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ForeignKey.on_delete) 88 | is used. This means the related object will also be deleted. You may also choose 89 | to set `null=True` when defining an address field to have the address set 90 | to Null instead of deleting the related object. For more information and an example, 91 | see the readme for the `django-address` example_site. 92 | 93 | ## Creation 94 | 95 | It can be created using the same optional arguments as a ForeignKey field. 96 | For example: 97 | 98 | ```python 99 | from address.models import AddressField 100 | 101 | class MyModel(models.Model): 102 | address1 = AddressField() 103 | address2 = AddressField(related_name='+', blank=True, null=True) 104 | ``` 105 | 106 | ## Setting Values 107 | 108 | Values can be set either by assigning an Address object: 109 | 110 | ```python 111 | addr = Address(...) 112 | addr.save() 113 | obj.address = addr 114 | ``` 115 | 116 | Or by supplying a dictionary of address components: 117 | 118 | ```python 119 | obj.address = {'street_number': '1', 'route': 'Somewhere Ave', ...} 120 | ``` 121 | 122 | The structure of the address components is as follows: 123 | 124 | ```python 125 | { 126 | 'raw': '1 Somewhere Ave, Northcote, VIC 3070, AU', 127 | 'street_number': '1', 128 | 'route': 'Somewhere Ave', 129 | 'locality': 'Northcote', 130 | 'postal_code': '3070', 131 | 'state': 'Victoria', 132 | 'state_code': 'VIC', 133 | 'country': 'Australia', 134 | 'country_code': 'AU' 135 | } 136 | ``` 137 | 138 | All except the `raw` field can be omitted. In addition, a raw address may 139 | be set directly: 140 | 141 | ```python 142 | obj.address = 'Out the back of 1 Somewhere Ave, Northcote, Australia' 143 | ``` 144 | 145 | ## Getting Values 146 | 147 | When accessed, the address field simply returns an Address object. This way 148 | all components may be accessed naturally through the object. For example:: 149 | 150 | ```python 151 | route = obj.address.route 152 | state_name = obj.address.locality.state.name 153 | ``` 154 | 155 | ## Forms 156 | 157 | Included is a form field for simplifying address entry. A Google maps 158 | auto-complete is performed in the browser and passed to the view. If 159 | the lookup fails the raw entered value is used. 160 | 161 | TODO: Talk about this more. 162 | 163 | ## Partial Example 164 | 165 | The model: 166 | 167 | ```python 168 | from address.models import AddressField 169 | 170 | class Person(models.Model): 171 | address = AddressField(on_delete=models.CASCADE) 172 | ``` 173 | 174 | The form: 175 | 176 | ```python 177 | from address.forms import AddressField 178 | 179 | class PersonForm(forms.Form): 180 | address = AddressField() 181 | ``` 182 | 183 | The template: 184 | 185 | ```html 186 | 187 | {{ form.media }} 188 | 189 | 190 | {{ form }} 191 | 192 | ``` 193 | 194 | ## Running Django-Address Tests 195 | Django-address currently has partial form and model test coverage using `django.test.TestCase`. 196 | 197 | To run the current tests: 198 | 199 | 1. [Clone](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) `django-address` locally. 200 | 1. Navigate to the example site, . `/django-address/example_site` 201 | 1. Create a [virtual environment](https://www.tangowithdjango.com/book17/chapters/requirements.html#virtual-environments) and install the example site dependencies. For example: 202 | 203 | ``` 204 | mkvirtualenv -p python3 django-address 205 | pip install -r requirements.txt 206 | ``` 207 | 1. Run `./manage.py test` 208 | 209 | ## Important note regarding US Territories 210 | Django-address does not currently support the parsing of US territories aka Protectorates such as Guam or Puerto Rico. 211 | 212 | This topic is under active consideration and its status is described in [#82](https://github.com/furious-luke/django-address/issues/82) 213 | 214 | ## Project Status Notes 215 | 216 | This library was created by [Luke Hodkinson](@furious-luke) originally focused on Australian addresses. 217 | 218 | In 2015 Luke began working to abstract the project so it could handle a wider variety of international addresses. 219 | 220 | This became the current `dev` branch. While good progress was made on this, the branch became stale and releases 221 | continued under the current model architecture on master. 222 | 223 | The project is currently in open development, read more about the project status [in this issue](https://github.com/furious-luke/django-address/issues/98). 224 | 225 | If you have questions, bug reports or suggestions please create a New Issue for the project. 226 | -------------------------------------------------------------------------------- /address/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "address.apps.AddressConfig" 2 | -------------------------------------------------------------------------------- /address/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.admin import SimpleListFilter 3 | 4 | from address.models import Country, State, Locality, Address 5 | 6 | 7 | class UnidentifiedListFilter(SimpleListFilter): 8 | title = "unidentified" 9 | parameter_name = "unidentified" 10 | 11 | def lookups(self, request, model_admin): 12 | return (("unidentified", "unidentified"),) 13 | 14 | def queryset(self, request, queryset): 15 | if self.value() == "unidentified": 16 | return queryset.filter(locality=None) 17 | 18 | 19 | @admin.register(Country) 20 | class CountryAdmin(admin.ModelAdmin): 21 | search_fields = ("name", "code") 22 | 23 | 24 | @admin.register(State) 25 | class StateAdmin(admin.ModelAdmin): 26 | search_fields = ("name", "code") 27 | 28 | 29 | @admin.register(Locality) 30 | class LocalityAdmin(admin.ModelAdmin): 31 | search_fields = ("name", "postal_code") 32 | 33 | 34 | @admin.register(Address) 35 | class AddressAdmin(admin.ModelAdmin): 36 | search_fields = ("street_number", "route", "raw") 37 | list_filter = (UnidentifiedListFilter,) 38 | -------------------------------------------------------------------------------- /address/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AddressConfig(AppConfig): 5 | """ 6 | Define config for the member app so that we can hook in signals. 7 | """ 8 | 9 | name = "address" 10 | default_auto_field = "django.db.models.AutoField" 11 | -------------------------------------------------------------------------------- /address/compat.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.db.models.fields.related import ForeignObject 3 | 4 | django_version = django.VERSION 5 | 6 | is_django2 = django_version >= (2, 0) 7 | 8 | 9 | def compat_contribute_to_class(self, cls, name, virtual_only=False): 10 | if is_django2: 11 | super(ForeignObject, self).contribute_to_class(cls, name, private_only=virtual_only) 12 | else: 13 | super(ForeignObject, self).contribute_to_class(cls, name, virtual_only=virtual_only) 14 | -------------------------------------------------------------------------------- /address/forms.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django import forms 4 | 5 | from .models import Address, to_python 6 | from .widgets import AddressWidget 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | __all__ = ["AddressWidget", "AddressField"] 11 | 12 | 13 | class AddressField(forms.ModelChoiceField): 14 | widget = AddressWidget 15 | 16 | def __init__(self, *args, **kwargs): 17 | kwargs["queryset"] = Address.objects.none() 18 | super(AddressField, self).__init__(*args, **kwargs) 19 | 20 | def to_python(self, value): 21 | 22 | # Treat `None`s and empty strings as empty. 23 | if value is None or value == "": 24 | return None 25 | 26 | # Check for garbage in the lat/lng components. 27 | for field in ["latitude", "longitude"]: 28 | if field in value: 29 | if value[field]: 30 | try: 31 | value[field] = float(value[field]) 32 | except Exception: 33 | raise forms.ValidationError( 34 | "Invalid value for %(field)s", 35 | code="invalid", 36 | params={"field": field}, 37 | ) 38 | else: 39 | value[field] = None 40 | 41 | return to_python(value) 42 | -------------------------------------------------------------------------------- /address/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Address", 14 | fields=[ 15 | ( 16 | "id", 17 | models.AutoField( 18 | verbose_name="ID", 19 | serialize=False, 20 | auto_created=True, 21 | primary_key=True, 22 | ), 23 | ), 24 | ("street_number", models.CharField(max_length=20, blank=True)), 25 | ("route", models.CharField(max_length=100, blank=True)), 26 | ("raw", models.CharField(max_length=200)), 27 | ("formatted", models.CharField(max_length=200, blank=True)), 28 | ("latitude", models.FloatField(null=True, blank=True)), 29 | ("longitude", models.FloatField(null=True, blank=True)), 30 | ], 31 | options={ 32 | "ordering": ("locality", "route", "street_number"), 33 | "verbose_name_plural": "Addresses", 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name="Country", 38 | fields=[ 39 | ( 40 | "id", 41 | models.AutoField( 42 | verbose_name="ID", 43 | serialize=False, 44 | auto_created=True, 45 | primary_key=True, 46 | ), 47 | ), 48 | ("name", models.CharField(unique=True, max_length=40, blank=True)), 49 | ("code", models.CharField(max_length=2, blank=True)), 50 | ], 51 | options={ 52 | "ordering": ("name",), 53 | "verbose_name_plural": "Countries", 54 | }, 55 | ), 56 | migrations.CreateModel( 57 | name="Locality", 58 | fields=[ 59 | ( 60 | "id", 61 | models.AutoField( 62 | verbose_name="ID", 63 | serialize=False, 64 | auto_created=True, 65 | primary_key=True, 66 | ), 67 | ), 68 | ("name", models.CharField(max_length=165, blank=True)), 69 | ("postal_code", models.CharField(max_length=10, blank=True)), 70 | ], 71 | options={ 72 | "ordering": ("state", "name"), 73 | "verbose_name_plural": "Localities", 74 | }, 75 | ), 76 | migrations.CreateModel( 77 | name="State", 78 | fields=[ 79 | ( 80 | "id", 81 | models.AutoField( 82 | verbose_name="ID", 83 | serialize=False, 84 | auto_created=True, 85 | primary_key=True, 86 | ), 87 | ), 88 | ("name", models.CharField(max_length=165, blank=True)), 89 | ("code", models.CharField(max_length=3, blank=True)), 90 | ( 91 | "country", 92 | models.ForeignKey( 93 | on_delete=models.CASCADE, 94 | related_name="states", 95 | to="address.Country", 96 | ), 97 | ), 98 | ], 99 | options={ 100 | "ordering": ("country", "name"), 101 | }, 102 | ), 103 | migrations.AddField( 104 | model_name="locality", 105 | name="state", 106 | field=models.ForeignKey(on_delete=models.CASCADE, related_name="localities", to="address.State"), 107 | ), 108 | migrations.AddField( 109 | model_name="address", 110 | name="locality", 111 | field=models.ForeignKey( 112 | on_delete=models.CASCADE, 113 | related_name="addresses", 114 | blank=True, 115 | to="address.Locality", 116 | null=True, 117 | ), 118 | ), 119 | migrations.AlterUniqueTogether( 120 | name="state", 121 | unique_together=set([("name", "country")]), 122 | ), 123 | migrations.AlterUniqueTogether( 124 | name="locality", 125 | unique_together=set([("name", "state")]), 126 | ), 127 | ] 128 | -------------------------------------------------------------------------------- /address/migrations/0002_auto_20160213_1726.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("address", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterUniqueTogether( 15 | name="locality", 16 | unique_together=set([("name", "postal_code", "state")]), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /address/migrations/0003_auto_20200830_1851.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-08-30 18:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("address", "0002_auto_20160213_1726"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="state", 15 | name="code", 16 | field=models.CharField(blank=True, max_length=8), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /address/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-luke/django-address/6e5b13859b8a795b08189dde7ce1aab4cca18827/address/migrations/__init__.py -------------------------------------------------------------------------------- /address/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.db import models 5 | 6 | try: 7 | from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor 8 | except ImportError: 9 | from django.db.models.fields.related import ( 10 | ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor, 11 | ) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | __all__ = ["Country", "State", "Locality", "Address", "AddressField"] 16 | 17 | 18 | class InconsistentDictError(Exception): 19 | pass 20 | 21 | 22 | def _to_python(value): 23 | raw = value.get("raw", "") 24 | country = value.get("country", "") 25 | country_code = value.get("country_code", "") 26 | state = value.get("state", "") 27 | state_code = value.get("state_code", "") 28 | locality = value.get("locality", "") 29 | sublocality = value.get("sublocality", "") 30 | postal_town = value.get("postal_town", "") 31 | postal_code = value.get("postal_code", "") 32 | street_number = value.get("street_number", "") 33 | route = value.get("route", "") 34 | formatted = value.get("formatted", "") 35 | latitude = value.get("latitude", None) 36 | longitude = value.get("longitude", None) 37 | 38 | # If there is no value (empty raw) then return None. 39 | if not raw: 40 | return None 41 | 42 | # Fix issue with NYC boroughs (https://code.google.com/p/gmaps-api-issues/issues/detail?id=635) 43 | if not locality and sublocality: 44 | locality = sublocality 45 | 46 | # Fix issue with UK addresses with no locality 47 | # (https://github.com/furious-luke/django-address/issues/114) 48 | if not locality and postal_town: 49 | locality = postal_town 50 | 51 | # If we have an inconsistent set of value bail out now. 52 | if (country or state or locality) and not (country and state and locality): 53 | raise InconsistentDictError 54 | 55 | # Handle the country. 56 | try: 57 | country_obj = Country.objects.get(name=country) 58 | except Country.DoesNotExist: 59 | if country: 60 | if len(country_code) > Country._meta.get_field("code").max_length: 61 | if country_code != country: 62 | raise ValueError("Invalid country code (too long): %s" % country_code) 63 | country_code = "" 64 | country_obj = Country.objects.create(name=country, code=country_code) 65 | else: 66 | country_obj = None 67 | 68 | # Handle the state. 69 | try: 70 | state_obj = State.objects.get(name=state, country=country_obj) 71 | except State.DoesNotExist: 72 | if state: 73 | if len(state_code) > State._meta.get_field("code").max_length: 74 | if state_code != state: 75 | raise ValueError("Invalid state code (too long): %s" % state_code) 76 | state_code = "" 77 | state_obj = State.objects.create(name=state, code=state_code, country=country_obj) 78 | else: 79 | state_obj = None 80 | 81 | # Handle the locality. 82 | try: 83 | locality_obj = Locality.objects.get(name=locality, postal_code=postal_code, state=state_obj) 84 | except Locality.DoesNotExist: 85 | if locality: 86 | locality_obj = Locality.objects.create(name=locality, postal_code=postal_code, state=state_obj) 87 | else: 88 | locality_obj = None 89 | 90 | # Handle the address. 91 | try: 92 | if not (street_number or route or locality): 93 | address_obj = Address.objects.get(raw=raw) 94 | else: 95 | address_obj = Address.objects.get(street_number=street_number, route=route, locality=locality_obj) 96 | except Address.DoesNotExist: 97 | address_obj = Address( 98 | street_number=street_number, 99 | route=route, 100 | raw=raw, 101 | locality=locality_obj, 102 | formatted=formatted, 103 | latitude=latitude, 104 | longitude=longitude, 105 | ) 106 | 107 | # If "formatted" is empty try to construct it from other values. 108 | if not address_obj.formatted: 109 | address_obj.formatted = str(address_obj) 110 | 111 | # Need to save. 112 | address_obj.save() 113 | 114 | # Done. 115 | return address_obj 116 | 117 | 118 | ## 119 | # Convert a dictionary to an address. 120 | ## 121 | 122 | 123 | def to_python(value): 124 | 125 | # Keep `None`s. 126 | if value is None: 127 | return None 128 | 129 | # Is it already an address object? 130 | if isinstance(value, Address): 131 | return value 132 | 133 | # If we have an integer, assume it is a model primary key. 134 | elif isinstance(value, int): 135 | return value 136 | 137 | # A string is considered a raw value. 138 | elif isinstance(value, str): 139 | obj = Address(raw=value) 140 | obj.save() 141 | return obj 142 | 143 | # A dictionary of named address components. 144 | elif isinstance(value, dict): 145 | 146 | # Attempt a conversion. 147 | try: 148 | return _to_python(value) 149 | except InconsistentDictError: 150 | return Address.objects.create(raw=value["raw"]) 151 | 152 | # Not in any of the formats I recognise. 153 | raise ValidationError("Invalid address value.") 154 | 155 | 156 | ## 157 | # A country. 158 | ## 159 | 160 | 161 | class Country(models.Model): 162 | name = models.CharField(max_length=40, unique=True, blank=True) 163 | code = models.CharField(max_length=2, blank=True) # not unique as there are duplicates (IT) 164 | 165 | class Meta: 166 | verbose_name_plural = "Countries" 167 | ordering = ("name",) 168 | 169 | def __str__(self): 170 | return "%s" % (self.name or self.code) 171 | 172 | 173 | ## 174 | # A state. Google refers to this as `administration_level_1`. 175 | ## 176 | 177 | 178 | class State(models.Model): 179 | name = models.CharField(max_length=165, blank=True) 180 | code = models.CharField(max_length=8, blank=True) 181 | country = models.ForeignKey(Country, on_delete=models.CASCADE, related_name="states") 182 | 183 | class Meta: 184 | unique_together = ("name", "country") 185 | ordering = ("country", "name") 186 | 187 | def __str__(self): 188 | txt = self.to_str() 189 | country = "%s" % self.country 190 | if country and txt: 191 | txt += ", " 192 | txt += country 193 | return txt 194 | 195 | def to_str(self): 196 | return "%s" % (self.name or self.code) 197 | 198 | 199 | ## 200 | # A locality (suburb). 201 | ## 202 | 203 | 204 | class Locality(models.Model): 205 | name = models.CharField(max_length=165, blank=True) 206 | postal_code = models.CharField(max_length=10, blank=True) 207 | state = models.ForeignKey(State, on_delete=models.CASCADE, related_name="localities") 208 | 209 | class Meta: 210 | verbose_name_plural = "Localities" 211 | unique_together = ("name", "postal_code", "state") 212 | ordering = ("state", "name") 213 | 214 | def __str__(self): 215 | txt = "%s" % self.name 216 | state = self.state.to_str() if self.state else "" 217 | if txt and state: 218 | txt += ", " 219 | txt += state 220 | if self.postal_code: 221 | txt += " %s" % self.postal_code 222 | cntry = "%s" % (self.state.country if self.state and self.state.country else "") 223 | if cntry: 224 | txt += ", %s" % cntry 225 | return txt 226 | 227 | 228 | ## 229 | # An address. If for any reason we are unable to find a matching 230 | # decomposed address we will store the raw address string in `raw`. 231 | ## 232 | 233 | 234 | class Address(models.Model): 235 | street_number = models.CharField(max_length=20, blank=True) 236 | route = models.CharField(max_length=100, blank=True) 237 | locality = models.ForeignKey( 238 | Locality, 239 | on_delete=models.CASCADE, 240 | related_name="addresses", 241 | blank=True, 242 | null=True, 243 | ) 244 | raw = models.CharField(max_length=200) 245 | formatted = models.CharField(max_length=200, blank=True) 246 | latitude = models.FloatField(blank=True, null=True) 247 | longitude = models.FloatField(blank=True, null=True) 248 | 249 | class Meta: 250 | verbose_name_plural = "Addresses" 251 | ordering = ("locality", "route", "street_number") 252 | 253 | def __str__(self): 254 | if self.formatted != "": 255 | txt = "%s" % self.formatted 256 | elif self.locality: 257 | txt = "" 258 | if self.street_number: 259 | txt = "%s" % self.street_number 260 | if self.route: 261 | if txt: 262 | txt += " %s" % self.route 263 | locality = "%s" % self.locality 264 | if txt and locality: 265 | txt += ", " 266 | txt += locality 267 | else: 268 | txt = "%s" % self.raw 269 | return txt 270 | 271 | def clean(self): 272 | if not self.raw: 273 | raise ValidationError("Addresses may not have a blank `raw` field.") 274 | 275 | def as_dict(self): 276 | ad = dict( 277 | street_number=self.street_number, 278 | route=self.route, 279 | raw=self.raw, 280 | formatted=self.formatted, 281 | latitude=self.latitude if self.latitude else "", 282 | longitude=self.longitude if self.longitude else "", 283 | ) 284 | if self.locality: 285 | ad["locality"] = self.locality.name 286 | ad["postal_code"] = self.locality.postal_code 287 | if self.locality.state: 288 | ad["state"] = self.locality.state.name 289 | ad["state_code"] = self.locality.state.code 290 | if self.locality.state.country: 291 | ad["country"] = self.locality.state.country.name 292 | ad["country_code"] = self.locality.state.country.code 293 | return ad 294 | 295 | 296 | class AddressDescriptor(ForwardManyToOneDescriptor): 297 | def __set__(self, inst, value): 298 | super(AddressDescriptor, self).__set__(inst, to_python(value)) 299 | 300 | 301 | ## 302 | # A field for addresses in other models. 303 | ## 304 | 305 | 306 | class AddressField(models.ForeignKey): 307 | description = "An address" 308 | 309 | def __init__(self, *args, **kwargs): 310 | kwargs["to"] = "address.Address" 311 | # The address should be set to null when deleted if the relationship could be null 312 | default_on_delete = models.SET_NULL if kwargs.get("null", False) else models.CASCADE 313 | kwargs["on_delete"] = kwargs.get("on_delete", default_on_delete) 314 | super(AddressField, self).__init__(*args, **kwargs) 315 | 316 | def contribute_to_class(self, cls, name, virtual_only=False): 317 | from address.compat import compat_contribute_to_class 318 | 319 | compat_contribute_to_class(self, cls, name, virtual_only) 320 | 321 | setattr(cls, self.name, AddressDescriptor(self)) 322 | 323 | def formfield(self, **kwargs): 324 | from .forms import AddressField as AddressFormField 325 | 326 | defaults = dict(form_class=AddressFormField) 327 | defaults.update(kwargs) 328 | return super(AddressField, self).formfield(**defaults) 329 | -------------------------------------------------------------------------------- /address/static/address/js/address.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $('input.address').each(function () { 3 | var self = $(this); 4 | var cmps = $('#' + self.attr('name') + '_components'); 5 | var fmtd = $('input[name="' + self.attr('name') + '_formatted"]'); 6 | self.geocomplete({ 7 | details: cmps, 8 | detailsAttribute: 'data-geo' 9 | }).change(function () { 10 | if (self.val() != fmtd.val()) { 11 | var cmp_names = [ 12 | 'country', 13 | 'country_code', 14 | 'locality', 15 | 'postal_code', 16 | 'postal_town', 17 | 'route', 18 | 'street_number', 19 | 'state', 20 | 'state_code', 21 | 'formatted', 22 | 'latitude', 23 | 'longitude', 24 | ]; 25 | 26 | for (var ii = 0; ii < cmp_names.length; ++ii) { 27 | $('input[name="' + self.attr('name') + '_' + cmp_names[ii] + '"]').val(''); 28 | } 29 | } 30 | }); 31 | }); 32 | }); -------------------------------------------------------------------------------- /address/static/js/jquery.geocomplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Geocoding and Places Autocomplete Plugin - V 1.7.0 3 | * 4 | * Includes modifications specific to django-address, see: 5 | * https://github.com/furious-luke/django-address/issues/119 6 | * 7 | * @author Martin Kleppe , 2016 8 | * @author Ubilabs http://ubilabs.net, 2016 9 | * @license MIT License 10 | */ 11 | 12 | // # $.geocomplete() 13 | // ## jQuery Geocoding and Places Autocomplete Plugin 14 | // 15 | // * https://github.com/ubilabs/geocomplete/ 16 | // * by Martin Kleppe 17 | 18 | (function($, window, document, undefined){ 19 | 20 | // ## Options 21 | // The default options for this plugin. 22 | // 23 | // * `map` - Might be a selector, an jQuery object or a DOM element. Default is `false` which shows no map. 24 | // * `details` - The container that should be populated with data. Defaults to `false` which ignores the setting. 25 | // * 'detailsScope' - Allows you to scope the 'details' container and have multiple geocomplete fields on one page. Must be a parent of the input. Default is 'null' 26 | // * `location` - Location to initialize the map on. Might be an address `string` or an `array` with [latitude, longitude] or a `google.maps.LatLng`object. Default is `false` which shows a blank map. 27 | // * `bounds` - Whether to snap geocode search to map bounds. Default: `true` if false search globally. Alternatively pass a custom `LatLngBounds object. 28 | // * `autoselect` - Automatically selects the highlighted item or the first item from the suggestions list on Enter. 29 | // * `detailsAttribute` - The attribute's name to use as an indicator. Default: `"name"` 30 | // * `mapOptions` - Options to pass to the `google.maps.Map` constructor. See the full list [here](http://code.google.com/apis/maps/documentation/javascript/reference.html#MapOptions). 31 | // * `mapOptions.zoom` - The inital zoom level. Default: `14` 32 | // * `mapOptions.scrollwheel` - Whether to enable the scrollwheel to zoom the map. Default: `false` 33 | // * `mapOptions.mapTypeId` - The map type. Default: `"roadmap"` 34 | // * `markerOptions` - The options to pass to the `google.maps.Marker` constructor. See the full list [here](http://code.google.com/apis/maps/documentation/javascript/reference.html#MarkerOptions). 35 | // * `markerOptions.draggable` - If the marker is draggable. Default: `false`. Set to true to enable dragging. 36 | // * `markerOptions.disabled` - Do not show marker. Default: `false`. Set to true to disable marker. 37 | // * `maxZoom` - The maximum zoom level too zoom in after a geocoding response. Default: `16` 38 | // * `types` - An array containing one or more of the supported types for the places request. Default: `['geocode']` See the full list [here](http://code.google.com/apis/maps/documentation/javascript/places.html#place_search_requests). 39 | // * `blur` - Trigger geocode when input loses focus. 40 | // * `geocodeAfterResult` - If blur is set to true, choose whether to geocode if user has explicitly selected a result before blur. 41 | // * `restoreValueAfterBlur` - Restores the input's value upon blurring. Default is `false` which ignores the setting. 42 | 43 | var defaults = { 44 | bounds: true, 45 | country: null, 46 | map: false, 47 | details: false, 48 | detailsAttribute: "name", 49 | detailsScope: null, 50 | autoselect: true, 51 | location: false, 52 | 53 | mapOptions: { 54 | zoom: 14, 55 | scrollwheel: false, 56 | mapTypeId: "roadmap" 57 | }, 58 | 59 | markerOptions: { 60 | draggable: false 61 | }, 62 | 63 | maxZoom: 16, 64 | types: ['geocode'], 65 | blur: false, 66 | geocodeAfterResult: false, 67 | restoreValueAfterBlur: false 68 | }; 69 | 70 | // See: [Geocoding Types](https://developers.google.com/maps/documentation/geocoding/#Types) 71 | // on Google Developers. 72 | var componentTypes = ("street_address route intersection political " + 73 | "country administrative_area_level_1 administrative_area_level_2 " + 74 | "administrative_area_level_3 colloquial_area locality sublocality " + 75 | "neighborhood premise subpremise postal_code postal_town natural_feature airport " + 76 | "park point_of_interest post_box street_number floor room " + 77 | "lat lng viewport location " + 78 | "formatted_address location_type bounds").split(" "); 79 | 80 | // See: [Places Details Responses](https://developers.google.com/maps/documentation/javascript/places#place_details_responses) 81 | // on Google Developers. 82 | var placesDetails = ("id place_id url website vicinity reference name rating " + 83 | "international_phone_number icon formatted_phone_number").split(" "); 84 | 85 | // The actual plugin constructor. 86 | function GeoComplete(input, options) { 87 | 88 | this.options = $.extend(true, {}, defaults, options); 89 | 90 | // This is a fix to allow types:[] not to be overridden by defaults 91 | // so search results includes everything 92 | if (options && options.types) { 93 | this.options.types = options.types; 94 | } 95 | 96 | this.input = input; 97 | this.$input = $(input); 98 | 99 | this._defaults = defaults; 100 | this._name = 'geocomplete'; 101 | 102 | this.init(); 103 | } 104 | 105 | // Initialize all parts of the plugin. 106 | $.extend(GeoComplete.prototype, { 107 | init: function(){ 108 | this.initMap(); 109 | this.initMarker(); 110 | this.initGeocoder(); 111 | this.initDetails(); 112 | this.initLocation(); 113 | }, 114 | 115 | // Initialize the map but only if the option `map` was set. 116 | // This will create a `map` within the given container 117 | // using the provided `mapOptions` or link to the existing map instance. 118 | initMap: function(){ 119 | if (!this.options.map){ return; } 120 | 121 | if (typeof this.options.map.setCenter == "function"){ 122 | this.map = this.options.map; 123 | return; 124 | } 125 | 126 | this.map = new google.maps.Map( 127 | $(this.options.map)[0], 128 | this.options.mapOptions 129 | ); 130 | 131 | // add click event listener on the map 132 | google.maps.event.addListener( 133 | this.map, 134 | 'click', 135 | $.proxy(this.mapClicked, this) 136 | ); 137 | 138 | // add dragend even listener on the map 139 | google.maps.event.addListener( 140 | this.map, 141 | 'dragend', 142 | $.proxy(this.mapDragged, this) 143 | ); 144 | 145 | // add idle even listener on the map 146 | google.maps.event.addListener( 147 | this.map, 148 | 'idle', 149 | $.proxy(this.mapIdle, this) 150 | ); 151 | 152 | google.maps.event.addListener( 153 | this.map, 154 | 'zoom_changed', 155 | $.proxy(this.mapZoomed, this) 156 | ); 157 | }, 158 | 159 | // Add a marker with the provided `markerOptions` but only 160 | // if the option was set. Additionally it listens for the `dragend` event 161 | // to notify the plugin about changes. 162 | initMarker: function(){ 163 | if (!this.map){ return; } 164 | var options = $.extend(this.options.markerOptions, { map: this.map }); 165 | 166 | if (options.disabled){ return; } 167 | 168 | this.marker = new google.maps.Marker(options); 169 | 170 | google.maps.event.addListener( 171 | this.marker, 172 | 'dragend', 173 | $.proxy(this.markerDragged, this) 174 | ); 175 | }, 176 | 177 | // Associate the input with the autocompleter and create a geocoder 178 | // to fall back when the autocompleter does not return a value. 179 | initGeocoder: function(){ 180 | 181 | // Indicates is user did select a result from the dropdown. 182 | var selected = false; 183 | 184 | var options = { 185 | types: this.options.types, 186 | bounds: this.options.bounds === true ? null : this.options.bounds, 187 | componentRestrictions: this.options.componentRestrictions 188 | }; 189 | 190 | if (this.options.country){ 191 | options.componentRestrictions = {country: this.options.country}; 192 | } 193 | 194 | this.autocomplete = new google.maps.places.Autocomplete( 195 | this.input, options 196 | ); 197 | 198 | this.geocoder = new google.maps.Geocoder(); 199 | 200 | // Bind autocomplete to map bounds but only if there is a map 201 | // and `options.bindToMap` is set to true. 202 | if (this.map && this.options.bounds === true){ 203 | this.autocomplete.bindTo('bounds', this.map); 204 | } 205 | 206 | // Watch `place_changed` events on the autocomplete input field. 207 | google.maps.event.addListener( 208 | this.autocomplete, 209 | 'place_changed', 210 | $.proxy(this.placeChanged, this) 211 | ); 212 | 213 | // Prevent parent form from being submitted if user hit enter. 214 | this.$input.on('keypress.' + this._name, function(event){ 215 | if (event.keyCode === 13){ return false; } 216 | }); 217 | 218 | // Assume that if user types anything after having selected a result, 219 | // the selected location is not valid any more. 220 | if (this.options.geocodeAfterResult === true){ 221 | this.$input.bind('keypress.' + this._name, $.proxy(function(){ 222 | if (event.keyCode != 9 && this.selected === true){ 223 | this.selected = false; 224 | } 225 | }, this)); 226 | } 227 | 228 | // Listen for "geocode" events and trigger find action. 229 | this.$input.bind('geocode.' + this._name, $.proxy(function(){ 230 | this.find(); 231 | }, this)); 232 | 233 | // Saves the previous input value 234 | this.$input.bind('geocode:result.' + this._name, $.proxy(function(){ 235 | this.lastInputVal = this.$input.val(); 236 | }, this)); 237 | 238 | // Trigger find action when input element is blurred out and user has 239 | // not explicitly selected a result. 240 | // (Useful for typing partial location and tabbing to the next field 241 | // or clicking somewhere else.) 242 | if (this.options.blur === true){ 243 | this.$input.on('blur.' + this._name, $.proxy(function(){ 244 | if (this.options.geocodeAfterResult === true && this.selected === true) { return; } 245 | 246 | if (this.options.restoreValueAfterBlur === true && this.selected === true) { 247 | setTimeout($.proxy(this.restoreLastValue, this), 0); 248 | } else { 249 | this.find(); 250 | } 251 | }, this)); 252 | } 253 | }, 254 | 255 | // Prepare a given DOM structure to be populated when we got some data. 256 | // This will cycle through the list of component types and map the 257 | // corresponding elements. 258 | initDetails: function(){ 259 | if (!this.options.details){ return; } 260 | 261 | if(this.options.detailsScope) { 262 | var $details = $(this.input).parents(this.options.detailsScope).find(this.options.details); 263 | } else { 264 | var $details = $(this.options.details); 265 | } 266 | 267 | var attribute = this.options.detailsAttribute, 268 | details = {}; 269 | 270 | function setDetail(value){ 271 | details[value] = $details.find("[" + attribute + "=" + value + "]"); 272 | } 273 | 274 | $.each(componentTypes, function(index, key){ 275 | setDetail(key); 276 | setDetail(key + "_short"); 277 | }); 278 | 279 | $.each(placesDetails, function(index, key){ 280 | setDetail(key); 281 | }); 282 | 283 | this.$details = $details; 284 | this.details = details; 285 | }, 286 | 287 | // Set the initial location of the plugin if the `location` options was set. 288 | // This method will care about converting the value into the right format. 289 | initLocation: function() { 290 | 291 | var location = this.options.location, latLng; 292 | 293 | if (!location) { return; } 294 | 295 | if (typeof location == 'string') { 296 | this.find(location); 297 | return; 298 | } 299 | 300 | if (location instanceof Array) { 301 | latLng = new google.maps.LatLng(location[0], location[1]); 302 | } 303 | 304 | if (location instanceof google.maps.LatLng){ 305 | latLng = location; 306 | } 307 | 308 | if (latLng){ 309 | if (this.map){ this.map.setCenter(latLng); } 310 | if (this.marker){ this.marker.setPosition(latLng); } 311 | } 312 | }, 313 | 314 | destroy: function(){ 315 | if (this.map) { 316 | google.maps.event.clearInstanceListeners(this.map); 317 | google.maps.event.clearInstanceListeners(this.marker); 318 | } 319 | 320 | this.autocomplete.unbindAll(); 321 | google.maps.event.clearInstanceListeners(this.autocomplete); 322 | google.maps.event.clearInstanceListeners(this.input); 323 | this.$input.removeData(); 324 | this.$input.off(this._name); 325 | this.$input.unbind('.' + this._name); 326 | }, 327 | 328 | // Look up a given address. If no `address` was specified it uses 329 | // the current value of the input. 330 | find: function(address){ 331 | this.geocode({ 332 | address: address || this.$input.val() 333 | }); 334 | }, 335 | 336 | // Requests details about a given location. 337 | // Additionally it will bias the requests to the provided bounds. 338 | geocode: function(request){ 339 | // Don't geocode if the requested address is empty 340 | if (!request.address) { 341 | return; 342 | } 343 | if (this.options.bounds && !request.bounds){ 344 | if (this.options.bounds === true){ 345 | request.bounds = this.map && this.map.getBounds(); 346 | } else { 347 | request.bounds = this.options.bounds; 348 | } 349 | } 350 | 351 | if (this.options.country){ 352 | request.region = this.options.country; 353 | } 354 | 355 | this.geocoder.geocode(request, $.proxy(this.handleGeocode, this)); 356 | }, 357 | 358 | // Get the selected result. If no result is selected on the list, then get 359 | // the first result from the list. 360 | selectFirstResult: function() { 361 | //$(".pac-container").hide(); 362 | 363 | var selected = ''; 364 | // Check if any result is selected. 365 | if ($(".pac-item-selected")[0]) { 366 | selected = '-selected'; 367 | } 368 | 369 | // Get the first suggestion's text. 370 | var $span1 = $(".pac-container:visible .pac-item" + selected + ":first span:nth-child(2)").text(); 371 | var $span2 = $(".pac-container:visible .pac-item" + selected + ":first span:nth-child(3)").text(); 372 | 373 | // Adds the additional information, if available. 374 | var firstResult = $span1; 375 | if ($span2) { 376 | firstResult += " - " + $span2; 377 | } 378 | 379 | this.$input.val(firstResult); 380 | 381 | return firstResult; 382 | }, 383 | 384 | // Restores the input value using the previous value if it exists 385 | restoreLastValue: function() { 386 | if (this.lastInputVal){ this.$input.val(this.lastInputVal); } 387 | }, 388 | 389 | // Handles the geocode response. If more than one results was found 390 | // it triggers the "geocode:multiple" events. If there was an error 391 | // the "geocode:error" event is fired. 392 | handleGeocode: function(results, status){ 393 | if (status === google.maps.GeocoderStatus.OK) { 394 | var result = results[0]; 395 | this.$input.val(result.formatted_address); 396 | this.update(result); 397 | 398 | if (results.length > 1){ 399 | this.trigger("geocode:multiple", results); 400 | } 401 | 402 | } else { 403 | this.trigger("geocode:error", status); 404 | } 405 | }, 406 | 407 | // Triggers a given `event` with optional `arguments` on the input. 408 | trigger: function(event, argument){ 409 | this.$input.trigger(event, [argument]); 410 | }, 411 | 412 | // Set the map to a new center by passing a `geometry`. 413 | // If the geometry has a viewport, the map zooms out to fit the bounds. 414 | // Additionally it updates the marker position. 415 | center: function(geometry){ 416 | if (geometry.viewport){ 417 | this.map.fitBounds(geometry.viewport); 418 | if (this.map.getZoom() > this.options.maxZoom){ 419 | this.map.setZoom(this.options.maxZoom); 420 | } 421 | } else { 422 | this.map.setZoom(this.options.maxZoom); 423 | this.map.setCenter(geometry.location); 424 | } 425 | 426 | if (this.marker){ 427 | this.marker.setPosition(geometry.location); 428 | this.marker.setAnimation(this.options.markerOptions.animation); 429 | } 430 | }, 431 | 432 | // Update the elements based on a single places or geocoding response 433 | // and trigger the "geocode:result" event on the input. 434 | update: function(result){ 435 | 436 | if (this.map){ 437 | this.center(result.geometry); 438 | } 439 | 440 | if (this.$details){ 441 | this.fillDetails(result); 442 | } 443 | 444 | this.trigger("geocode:result", result); 445 | }, 446 | 447 | // Populate the provided elements with new `result` data. 448 | // This will lookup all elements that has an attribute with the given 449 | // component type. 450 | fillDetails: function(result){ 451 | 452 | var data = {}, 453 | geometry = result.geometry, 454 | viewport = geometry.viewport, 455 | bounds = geometry.bounds; 456 | 457 | // Create a simplified version of the address components. 458 | $.each(result.address_components, function(index, object){ 459 | var name = object.types[0]; 460 | 461 | $.each(object.types, function(index, name){ 462 | data[name] = object.long_name; 463 | data[name + "_short"] = object.short_name; 464 | }); 465 | }); 466 | 467 | // Add properties of the places details. 468 | $.each(placesDetails, function(index, key){ 469 | data[key] = result[key]; 470 | }); 471 | 472 | // Add infos about the address and geometry. 473 | $.extend(data, { 474 | formatted_address: result.formatted_address, 475 | location_type: geometry.location_type || "PLACES", 476 | viewport: viewport, 477 | bounds: bounds, 478 | location: geometry.location, 479 | lat: geometry.location.lat(), 480 | lng: geometry.location.lng() 481 | }); 482 | 483 | // Set the values for all details. 484 | $.each(this.details, $.proxy(function(key, $detail){ 485 | var value = data[key]; 486 | this.setDetail($detail, value); 487 | }, this)); 488 | 489 | this.data = data; 490 | }, 491 | 492 | // Assign a given `value` to a single `$element`. 493 | // If the element is an input, the value is set, otherwise it updates 494 | // the text content. 495 | setDetail: function($element, value){ 496 | 497 | if (value === undefined){ 498 | value = ""; 499 | } else if (typeof value.toUrlValue == "function"){ 500 | value = value.toUrlValue(); 501 | } 502 | 503 | if ($element.is(":input")){ 504 | $element.val(value); 505 | } else { 506 | $element.text(value); 507 | } 508 | }, 509 | 510 | // Fire the "geocode:dragged" event and pass the new position. 511 | markerDragged: function(event){ 512 | this.trigger("geocode:dragged", event.latLng); 513 | }, 514 | 515 | mapClicked: function(event) { 516 | this.trigger("geocode:click", event.latLng); 517 | }, 518 | 519 | // Fire the "geocode:mapdragged" event and pass the current position of the map center. 520 | mapDragged: function(event) { 521 | this.trigger("geocode:mapdragged", this.map.getCenter()); 522 | }, 523 | 524 | // Fire the "geocode:idle" event and pass the current position of the map center. 525 | mapIdle: function(event) { 526 | this.trigger("geocode:idle", this.map.getCenter()); 527 | }, 528 | 529 | mapZoomed: function(event) { 530 | this.trigger("geocode:zoom", this.map.getZoom()); 531 | }, 532 | 533 | // Restore the old position of the marker to the last knwon location. 534 | resetMarker: function(){ 535 | this.marker.setPosition(this.data.location); 536 | this.setDetail(this.details.lat, this.data.location.lat()); 537 | this.setDetail(this.details.lng, this.data.location.lng()); 538 | }, 539 | 540 | // Update the plugin after the user has selected an autocomplete entry. 541 | // If the place has no geometry it passes it to the geocoder. 542 | placeChanged: function(){ 543 | var place = this.autocomplete.getPlace(); 544 | this.selected = true; 545 | 546 | if (!place.geometry){ 547 | if (this.options.autoselect) { 548 | // Automatically selects the highlighted item or the first item from the 549 | // suggestions list. 550 | var autoSelection = this.selectFirstResult(); 551 | this.find(autoSelection); 552 | } 553 | } else { 554 | // Use the input text if it already gives geometry. 555 | this.update(place); 556 | } 557 | } 558 | }); 559 | 560 | // A plugin wrapper around the constructor. 561 | // Pass `options` with all settings that are different from the default. 562 | // The attribute is used to prevent multiple instantiations of the plugin. 563 | $.fn.geocomplete = function(options) { 564 | 565 | var attribute = 'plugin_geocomplete'; 566 | 567 | // If you call `.geocomplete()` with a string as the first parameter 568 | // it returns the corresponding property or calls the method with the 569 | // following arguments. 570 | if (typeof options == "string"){ 571 | 572 | var instance = $(this).data(attribute) || $(this).geocomplete().data(attribute), 573 | prop = instance[options]; 574 | 575 | if (typeof prop == "function"){ 576 | prop.apply(instance, Array.prototype.slice.call(arguments, 1)); 577 | return $(this); 578 | } else { 579 | if (arguments.length == 2){ 580 | prop = arguments[1]; 581 | } 582 | return prop; 583 | } 584 | } else { 585 | return this.each(function() { 586 | // Prevent against multiple instantiations. 587 | var instance = $.data(this, attribute); 588 | if (!instance) { 589 | instance = new GeoComplete( this, options ); 590 | $.data(this, attribute, instance); 591 | } 592 | }); 593 | } 594 | }; 595 | 596 | })( jQuery, window, document ); 597 | -------------------------------------------------------------------------------- /address/static/js/jquery.geocomplete.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Geocoding and Places Autocomplete Plugin - V 1.7.0 3 | * 4 | * @author Martin Kleppe , 2016 5 | * @author Ubilabs http://ubilabs.net, 2016 6 | * @license MIT License 7 | */ 8 | (function($,window,document,undefined){var defaults={bounds:true,country:null,map:false,details:false,detailsAttribute:"name",detailsScope:null,autoselect:true,location:false,mapOptions:{zoom:14,scrollwheel:false,mapTypeId:"roadmap"},markerOptions:{draggable:false},maxZoom:16,types:["geocode"],blur:false,geocodeAfterResult:false,restoreValueAfterBlur:false};var componentTypes=("street_address route intersection political "+"country administrative_area_level_1 administrative_area_level_2 "+"administrative_area_level_3 colloquial_area locality sublocality "+"neighborhood premise subpremise postal_code postal_town natural_feature airport "+"park point_of_interest post_box street_number floor room "+"lat lng viewport location "+"formatted_address location_type bounds").split(" ");var placesDetails=("id place_id url website vicinity reference name rating "+"international_phone_number icon formatted_phone_number").split(" ");function GeoComplete(input,options){this.options=$.extend(true,{},defaults,options);if(options&&options.types){this.options.types=options.types}this.input=input;this.$input=$(input);this._defaults=defaults;this._name="geocomplete";this.init()}$.extend(GeoComplete.prototype,{init:function(){this.initMap();this.initMarker();this.initGeocoder();this.initDetails();this.initLocation()},initMap:function(){if(!this.options.map){return}if(typeof this.options.map.setCenter=="function"){this.map=this.options.map;return}this.map=new google.maps.Map($(this.options.map)[0],this.options.mapOptions);google.maps.event.addListener(this.map,"click",$.proxy(this.mapClicked,this));google.maps.event.addListener(this.map,"dragend",$.proxy(this.mapDragged,this));google.maps.event.addListener(this.map,"idle",$.proxy(this.mapIdle,this));google.maps.event.addListener(this.map,"zoom_changed",$.proxy(this.mapZoomed,this))},initMarker:function(){if(!this.map){return}var options=$.extend(this.options.markerOptions,{map:this.map});if(options.disabled){return}this.marker=new google.maps.Marker(options);google.maps.event.addListener(this.marker,"dragend",$.proxy(this.markerDragged,this))},initGeocoder:function(){var selected=false;var options={types:this.options.types,bounds:this.options.bounds===true?null:this.options.bounds,componentRestrictions:this.options.componentRestrictions};if(this.options.country){options.componentRestrictions={country:this.options.country}}this.autocomplete=new google.maps.places.Autocomplete(this.input,options);this.geocoder=new google.maps.Geocoder;if(this.map&&this.options.bounds===true){this.autocomplete.bindTo("bounds",this.map)}google.maps.event.addListener(this.autocomplete,"place_changed",$.proxy(this.placeChanged,this));this.$input.on("keypress."+this._name,function(event){if(event.keyCode===13){return false}});if(this.options.geocodeAfterResult===true){this.$input.bind("keypress."+this._name,$.proxy(function(){if(event.keyCode!=9&&this.selected===true){this.selected=false}},this))}this.$input.bind("geocode."+this._name,$.proxy(function(){this.find()},this));this.$input.bind("geocode:result."+this._name,$.proxy(function(){this.lastInputVal=this.$input.val()},this));if(this.options.blur===true){this.$input.on("blur."+this._name,$.proxy(function(){if(this.options.geocodeAfterResult===true&&this.selected===true){return}if(this.options.restoreValueAfterBlur===true&&this.selected===true){setTimeout($.proxy(this.restoreLastValue,this),0)}else{this.find()}},this))}},initDetails:function(){if(!this.options.details){return}if(this.options.detailsScope){var $details=$(this.input).parents(this.options.detailsScope).find(this.options.details)}else{var $details=$(this.options.details)}var attribute=this.options.detailsAttribute,details={};function setDetail(value){details[value]=$details.find("["+attribute+"="+value+"]")}$.each(componentTypes,function(index,key){setDetail(key);setDetail(key+"_short")});$.each(placesDetails,function(index,key){setDetail(key)});this.$details=$details;this.details=details},initLocation:function(){var location=this.options.location,latLng;if(!location){return}if(typeof location=="string"){this.find(location);return}if(location instanceof Array){latLng=new google.maps.LatLng(location[0],location[1])}if(location instanceof google.maps.LatLng){latLng=location}if(latLng){if(this.map){this.map.setCenter(latLng)}if(this.marker){this.marker.setPosition(latLng)}}},destroy:function(){if(this.map){google.maps.event.clearInstanceListeners(this.map);google.maps.event.clearInstanceListeners(this.marker)}this.autocomplete.unbindAll();google.maps.event.clearInstanceListeners(this.autocomplete);google.maps.event.clearInstanceListeners(this.input);this.$input.removeData();this.$input.off(this._name);this.$input.unbind("."+this._name)},find:function(address){this.geocode({address:address||this.$input.val()})},geocode:function(request){if(!request.address){return}if(this.options.bounds&&!request.bounds){if(this.options.bounds===true){request.bounds=this.map&&this.map.getBounds()}else{request.bounds=this.options.bounds}}if(this.options.country){request.region=this.options.country}this.geocoder.geocode(request,$.proxy(this.handleGeocode,this))},selectFirstResult:function(){var selected="";if($(".pac-item-selected")[0]){selected="-selected"}var $span1=$(".pac-container:visible .pac-item"+selected+":first span:nth-child(2)").text();var $span2=$(".pac-container:visible .pac-item"+selected+":first span:nth-child(3)").text();var firstResult=$span1;if($span2){firstResult+=" - "+$span2}this.$input.val(firstResult);return firstResult},restoreLastValue:function(){if(this.lastInputVal){this.$input.val(this.lastInputVal)}},handleGeocode:function(results,status){if(status===google.maps.GeocoderStatus.OK){var result=results[0];this.$input.val(result.formatted_address);this.update(result);if(results.length>1){this.trigger("geocode:multiple",results)}}else{this.trigger("geocode:error",status)}},trigger:function(event,argument){this.$input.trigger(event,[argument])},center:function(geometry){if(geometry.viewport){this.map.fitBounds(geometry.viewport);if(this.map.getZoom()>this.options.maxZoom){this.map.setZoom(this.options.maxZoom)}}else{this.map.setZoom(this.options.maxZoom);this.map.setCenter(geometry.location)}if(this.marker){this.marker.setPosition(geometry.location);this.marker.setAnimation(this.options.markerOptions.animation)}},update:function(result){if(this.map){this.center(result.geometry)}if(this.$details){this.fillDetails(result)}this.trigger("geocode:result",result)},fillDetails:function(result){var data={},geometry=result.geometry,viewport=geometry.viewport,bounds=geometry.bounds;$.each(result.address_components,function(index,object){var name=object.types[0];$.each(object.types,function(index,name){data[name]=object.long_name;data[name+"_short"]=object.short_name})});$.each(placesDetails,function(index,key){data[key]=result[key]});$.extend(data,{formatted_address:result.formatted_address,location_type:geometry.location_type||"PLACES",viewport:viewport,bounds:bounds,location:geometry.location,lat:geometry.location.lat(),lng:geometry.location.lng()});$.each(this.details,$.proxy(function(key,$detail){var value=data[key];this.setDetail($detail,value)},this));this.data=data},setDetail:function($element,value){if(value===undefined){value=""}else if(typeof value.toUrlValue=="function"){value=value.toUrlValue()}if($element.is(":input")){$element.val(value)}else{$element.text(value)}},markerDragged:function(event){this.trigger("geocode:dragged",event.latLng)},mapClicked:function(event){this.trigger("geocode:click",event.latLng)},mapDragged:function(event){this.trigger("geocode:mapdragged",this.map.getCenter())},mapIdle:function(event){this.trigger("geocode:idle",this.map.getCenter())},mapZoomed:function(event){this.trigger("geocode:zoom",this.map.getZoom())},resetMarker:function(){this.marker.setPosition(this.data.location);this.setDetail(this.details.lat,this.data.location.lat());this.setDetail(this.details.lng,this.data.location.lng())},placeChanged:function(){var place=this.autocomplete.getPlace();this.selected=true;if(!place.geometry){if(this.options.autoselect){var autoSelection=this.selectFirstResult();this.find(autoSelection)}}else{this.update(place)}}});$.fn.geocomplete=function(options){var attribute="plugin_geocomplete";if(typeof options=="string"){var instance=$(this).data(attribute)||$(this).geocomplete().data(attribute),prop=instance[options];if(typeof prop=="function"){prop.apply(instance,Array.prototype.slice.call(arguments,1));return $(this)}else{if(arguments.length==2){prop=arguments[1]}return prop}}else{return this.each(function(){var instance=$.data(this,attribute);if(!instance){instance=new GeoComplete(this,options);$.data(this,attribute,instance)}})}}})(jQuery,window,document); 9 | -------------------------------------------------------------------------------- /address/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-luke/django-address/6e5b13859b8a795b08189dde7ce1aab4cca18827/address/tests/__init__.py -------------------------------------------------------------------------------- /address/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.forms import ValidationError, Form 3 | from address.forms import AddressField, AddressWidget 4 | 5 | 6 | class TestForm(Form): 7 | address = AddressField() 8 | 9 | 10 | class AddressFieldTestCase(TestCase): 11 | def setUp(self): 12 | self.form = TestForm() 13 | self.field = self.form.base_fields["address"] 14 | self.missing_state = { 15 | "country": "UK", 16 | "locality": "Somewhere", 17 | "postal_code": "34904", 18 | "route": "A street?", 19 | "street_number": "3", 20 | "raw": "3 A street?, Somewhere, UK", 21 | } 22 | 23 | def test_to_python_none(self): 24 | self.assertEqual(self.field.to_python(None), None) 25 | 26 | def test_to_python_empty(self): 27 | self.assertEqual(self.field.to_python(""), None) 28 | 29 | def test_to_python_invalid_lat_lng(self): 30 | self.assertRaises(ValidationError, self.field.to_python, {"latitude": "x"}) 31 | self.assertRaises(ValidationError, self.field.to_python, {"longitude": "x"}) 32 | 33 | def test_to_python_invalid_empty_lat_lng(self): 34 | self.assertEqual(self.field.to_python({"latitude": ""}), None) 35 | self.assertEqual(self.field.to_python({"longitude": ""}), None) 36 | 37 | def test_to_python_no_locality(self): 38 | input = { 39 | "country": "United States", 40 | "country_code": "US", 41 | "state": "New York", 42 | "state_code": "NY", 43 | "locality": "", 44 | "sublocality": "Brooklyn", 45 | "postal_code": "11201", 46 | "route": "Joralemon St", 47 | "street_number": "209", 48 | "raw": "209 Joralemon Street, Brooklyn, NY, United States", 49 | } 50 | res = self.field.to_python(input) 51 | self.assertEqual(res.locality.name, "Brooklyn") 52 | 53 | def test_to_python_postal_town(self): 54 | """UK addresses with no `locality`, but a populated `postal_town`, should use the 55 | `postal_town` as the `locality`""" 56 | data = { 57 | "raw": "High Street, Leamington Spa", 58 | "route": "High Street", 59 | "postal_town": "Leamington Spa", 60 | "state": "England", 61 | "state_code": "England", 62 | "country": "United Kingdom", 63 | "country_code": "GB", 64 | "postal_code": "CV31", 65 | "formatted": "High St, Royal Leamington Spa, Leamington Spa CV31, UK", 66 | } 67 | address = self.field.to_python(data) 68 | self.assertIsNotNone(address.locality) 69 | self.assertEqual(address.locality.name, data["postal_town"]) 70 | 71 | # TODO: Fix 72 | # def test_to_python_empty_state(self): 73 | # val = self.field.to_python(self.missing_state) 74 | # self.assertTrue(isinstance(val, Address)) 75 | # self.assertNotEqual(val.locality, None) 76 | 77 | def test_to_python(self): 78 | res = self.field.to_python({"raw": "Someplace"}) 79 | self.assertEqual(res.raw, "Someplace") 80 | 81 | def test_render(self): 82 | # TODO: Check return value. 83 | self.form.as_table() 84 | 85 | 86 | class AddressWidgetTestCase(TestCase): 87 | def test_attributes_set_correctly(self): 88 | wid = AddressWidget(attrs={"size": "150"}) 89 | self.assertEqual(wid.attrs["size"], "150") 90 | html = wid.render("test", None) 91 | self.assertNotEqual(html.find('size="150"'), -1) 92 | -------------------------------------------------------------------------------- /address/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.db import IntegrityError 3 | from django.core.exceptions import ValidationError 4 | 5 | from address.models import Country, State, Locality, Address, AddressField 6 | from address.models import to_python 7 | 8 | # Python 3 fixes. 9 | import sys 10 | 11 | if sys.version > "3": 12 | long = int 13 | basestring = (str, bytes) 14 | unicode = str 15 | 16 | 17 | class CountryTestCase(TestCase): 18 | def setUp(self): 19 | self.au = Country.objects.create(name="Australia", code="AU") 20 | self.nz = Country.objects.create(name="New Zealand", code="NZ") 21 | self.be = Country.objects.create(name="Belgium", code="BE") 22 | 23 | def test_ordering(self): 24 | qs = Country.objects.all() 25 | self.assertEqual(qs.count(), 3) 26 | self.assertEqual(qs[0].code, "AU") 27 | self.assertEqual(qs[1].code, "BE") 28 | self.assertEqual(qs[2].code, "NZ") 29 | 30 | def test_unique_name(self): 31 | self.assertRaises(IntegrityError, Country.objects.create, name="Australia", code="**") 32 | 33 | def test_unicode(self): 34 | self.assertEqual(unicode(self.au), u"Australia") 35 | 36 | 37 | class StateTestCase(TestCase): 38 | def setUp(self): 39 | self.au = Country.objects.create(name="Australia", code="AU") 40 | self.vic = State.objects.create(name="Victoria", code="VIC", country=self.au) 41 | self.tas = State.objects.create(name="Tasmania", code="TAS", country=self.au) 42 | self.qld = State.objects.create(name="Queensland", country=self.au) 43 | self.empty = State.objects.create(country=self.au) 44 | self.uk = Country.objects.create(name="United Kingdom", code="UK") 45 | self.uk_vic = State.objects.create(name="Victoria", code="VIC", country=self.uk) 46 | 47 | def test_required_country(self): 48 | self.assertRaises(IntegrityError, State.objects.create) 49 | 50 | def test_ordering(self): 51 | qs = State.objects.all() 52 | self.assertEqual(qs.count(), 5) 53 | self.assertEqual(qs[0].name, "") 54 | self.assertEqual(qs[1].name, "Queensland") 55 | self.assertEqual(qs[2].name, "Tasmania") 56 | self.assertEqual(qs[3].name, "Victoria") 57 | self.assertEqual(qs[4].name, "Victoria") 58 | 59 | def test_unique_name_country(self): 60 | State.objects.create(name="Tasmania", country=self.uk) 61 | self.assertRaises(IntegrityError, State.objects.create, name="Tasmania", country=self.au) 62 | 63 | def test_unicode(self): 64 | self.assertEqual(unicode(self.vic), u"Victoria, Australia") 65 | self.assertEqual(unicode(self.empty), u"Australia") 66 | 67 | 68 | class LocalityTestCase(TestCase): 69 | def setUp(self): 70 | self.au = Country.objects.create(name="Australia", code="AU") 71 | self.uk = Country.objects.create(name="United Kingdom", code="UK") 72 | 73 | self.au_vic = State.objects.create(name="Victoria", code="VIC", country=self.au) 74 | self.au_tas = State.objects.create(name="Tasmania", code="TAS", country=self.au) 75 | self.au_qld = State.objects.create(name="Queensland", country=self.au) 76 | self.au_empty = State.objects.create(country=self.au) 77 | self.uk_vic = State.objects.create(name="Victoria", code="VIC", country=self.uk) 78 | 79 | self.au_vic_nco = Locality.objects.create(name="Northcote", postal_code="3070", state=self.au_vic) 80 | self.au_vic_mel = Locality.objects.create(name="Melbourne", postal_code="3000", state=self.au_vic) 81 | self.au_vic_ftz = Locality.objects.create(name="Fitzroy", state=self.au_vic) 82 | self.au_vic_empty = Locality.objects.create(state=self.au_vic) 83 | self.uk_vic_mel = Locality.objects.create(name="Melbourne", postal_code="3000", state=self.uk_vic) 84 | 85 | def test_required_state(self): 86 | self.assertRaises(IntegrityError, Locality.objects.create) 87 | 88 | def test_ordering(self): 89 | qs = Locality.objects.all() 90 | self.assertEqual(qs.count(), 5) 91 | self.assertEqual(qs[0].name, "") 92 | self.assertEqual(qs[1].name, "Fitzroy") 93 | self.assertEqual(qs[2].name, "Melbourne") 94 | self.assertEqual(qs[3].name, "Northcote") 95 | self.assertEqual(qs[4].name, "Melbourne") 96 | 97 | def test_unicode(self): 98 | self.assertEqual(unicode(self.au_vic_mel), u"Melbourne, Victoria 3000, Australia") 99 | self.assertEqual(unicode(self.au_vic_ftz), u"Fitzroy, Victoria, Australia") 100 | self.assertEqual(unicode(self.au_vic_empty), u"Victoria, Australia") 101 | 102 | 103 | class AddressTestCase(TestCase): 104 | def setUp(self): 105 | self.au = Country.objects.create(name="Australia", code="AU") 106 | self.uk = Country.objects.create(name="United Kingdom", code="UK") 107 | 108 | self.au_vic = State.objects.create(name="Victoria", code="VIC", country=self.au) 109 | self.au_tas = State.objects.create(name="Tasmania", code="TAS", country=self.au) 110 | self.au_qld = State.objects.create(name="Queensland", country=self.au) 111 | self.au_empty = State.objects.create(country=self.au) 112 | self.uk_vic = State.objects.create(name="Victoria", code="VIC", country=self.uk) 113 | 114 | self.au_vic_nco = Locality.objects.create(name="Northcote", postal_code="3070", state=self.au_vic) 115 | self.au_vic_mel = Locality.objects.create(name="Melbourne", postal_code="3000", state=self.au_vic) 116 | self.au_vic_ftz = Locality.objects.create(name="Fitzroy", state=self.au_vic) 117 | self.au_vic_empty = Locality.objects.create(state=self.au_vic) 118 | self.uk_vic_mel = Locality.objects.create(name="Melbourne", postal_code="3000", state=self.uk_vic) 119 | 120 | self.ad1 = Address.objects.create( 121 | street_number="1", 122 | route="Some Street", 123 | locality=self.au_vic_mel, 124 | raw="1 Some Street, Victoria, Melbourne", 125 | ) 126 | self.ad2 = Address.objects.create( 127 | street_number="10", 128 | route="Other Street", 129 | locality=self.au_vic_mel, 130 | raw="10 Other Street, Victoria, Melbourne", 131 | ) 132 | self.ad3 = Address.objects.create( 133 | street_number="1", 134 | route="Some Street", 135 | locality=self.au_vic_nco, 136 | raw="1 Some Street, Northcote, Victoria", 137 | ) 138 | self.ad_empty = Address.objects.create(locality=self.au_vic_nco, raw="Northcote, Victoria") 139 | 140 | def test_required_raw(self): 141 | obj = Address.objects.create() 142 | self.assertRaises(ValidationError, obj.clean) 143 | 144 | def test_ordering(self): 145 | qs = Address.objects.all() 146 | self.assertEqual(qs.count(), 4) 147 | self.assertEqual(qs[0].route, "Other Street") 148 | self.assertEqual(qs[1].route, "Some Street") 149 | self.assertEqual(qs[2].route, "") 150 | self.assertEqual(qs[3].route, "Some Street") 151 | 152 | # def test_unique_street_address_locality(self): 153 | # Address.objects.create(street_number='10', route='Other Street', locality=self.au_vic_nco) 154 | # self.assertRaises( 155 | # IntegrityError, Address.objects.create, 156 | # street_number='10', route='Other Street', locality=self.au_vic_mel 157 | # ) 158 | 159 | def test_unicode(self): 160 | self.assertEqual(unicode(self.ad1), u"1 Some Street, Melbourne, Victoria 3000, Australia") 161 | self.assertEqual(unicode(self.ad_empty), u"Northcote, Victoria 3070, Australia") 162 | 163 | 164 | class AddressFieldTestCase(TestCase): 165 | class TestModel(object): 166 | address = AddressField() 167 | 168 | def setUp(self): 169 | self.ad1_dict = { 170 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 171 | "street_number": "1", 172 | "route": "Somewhere Street", 173 | "locality": "Northcote", 174 | "postal_code": "3070", 175 | "state": "Victoria", 176 | "state_code": "VIC", 177 | "country": "Australia", 178 | "country_code": "AU", 179 | } 180 | self.test = self.TestModel() 181 | 182 | def test_assignment_from_dict(self): 183 | self.test.address = to_python(self.ad1_dict) 184 | self.assertEqual(self.test.address.raw, self.ad1_dict["raw"]) 185 | self.assertEqual(self.test.address.street_number, self.ad1_dict["street_number"]) 186 | self.assertEqual(self.test.address.route, self.ad1_dict["route"]) 187 | self.assertEqual(self.test.address.locality.name, self.ad1_dict["locality"]) 188 | self.assertEqual(self.test.address.locality.postal_code, self.ad1_dict["postal_code"]) 189 | self.assertEqual(self.test.address.locality.state.name, self.ad1_dict["state"]) 190 | self.assertEqual(self.test.address.locality.state.code, self.ad1_dict["state_code"]) 191 | self.assertEqual(self.test.address.locality.state.country.name, self.ad1_dict["country"]) 192 | self.assertEqual(self.test.address.locality.state.country.code, self.ad1_dict["country_code"]) 193 | 194 | def test_assignment_from_dict_no_country(self): 195 | ad = { 196 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 197 | "street_number": "1", 198 | "route": "Somewhere Street", 199 | "locality": "Northcote", 200 | "state": "Victoria", 201 | } 202 | self.test.address = to_python(ad) 203 | self.assertEqual(self.test.address.raw, ad["raw"]) 204 | self.assertEqual(self.test.address.street_number, "") 205 | self.assertEqual(self.test.address.route, "") 206 | self.assertEqual(self.test.address.locality, None) 207 | 208 | def test_assignment_from_dict_no_state(self): 209 | ad = { 210 | "raw": "Somewhere", 211 | "locality": "Northcote", 212 | "country": "Australia", 213 | } 214 | self.test.address = to_python(ad) 215 | self.assertEqual(self.test.address.raw, ad["raw"]) 216 | self.assertEqual(self.test.address.street_number, "") 217 | self.assertEqual(self.test.address.route, "") 218 | self.assertEqual(self.test.address.locality, None) 219 | 220 | def test_assignment_from_dict_no_locality(self): 221 | ad = { 222 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 223 | "street_number": "1", 224 | "route": "Somewhere Street", 225 | "state": "Victoria", 226 | "country": "Australia", 227 | } 228 | self.test.address = to_python(ad) 229 | self.assertEqual(self.test.address.raw, ad["raw"]) 230 | self.assertEqual(self.test.address.street_number, "") 231 | self.assertEqual(self.test.address.route, "") 232 | self.assertEqual(self.test.address.locality, None) 233 | 234 | def test_assignment_from_dict_only_address(self): 235 | ad = { 236 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 237 | "street_number": "1", 238 | "route": "Somewhere Street", 239 | } 240 | self.test.address = to_python(ad) 241 | self.assertEqual(self.test.address.raw, ad["raw"]) 242 | self.assertEqual(self.test.address.street_number, ad["street_number"]) 243 | self.assertEqual(self.test.address.route, ad["route"]) 244 | self.assertEqual(self.test.address.locality, None) 245 | 246 | def test_assignment_from_dict_duplicate_country_code(self): 247 | ad = { 248 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 249 | "street_number": "1", 250 | "route": "Somewhere Street", 251 | "locality": "Northcote", 252 | "state": "Victoria", 253 | "country": "Australia", 254 | "country_code": "Australia", 255 | } 256 | self.test.address = to_python(ad) 257 | self.assertEqual(self.test.address.raw, ad["raw"]) 258 | self.assertEqual(self.test.address.street_number, "1") 259 | self.assertEqual(self.test.address.route, "Somewhere Street") 260 | self.assertEqual(self.test.address.locality.name, "Northcote") 261 | self.assertEqual(self.test.address.locality.state.name, "Victoria") 262 | self.assertEqual(self.test.address.locality.state.country.name, "Australia") 263 | self.assertEqual(self.test.address.locality.state.country.code, "") 264 | 265 | def test_assignment_from_dict_duplicate_state_code(self): 266 | ad = { 267 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 268 | "street_number": "1", 269 | "route": "Somewhere Street", 270 | "locality": "Northcote", 271 | "state": "Victoria", 272 | "state_code": "Victoria", 273 | "country": "Australia", 274 | } 275 | self.test.address = to_python(ad) 276 | self.assertEqual(self.test.address.raw, ad["raw"]) 277 | self.assertEqual(self.test.address.street_number, "1") 278 | self.assertEqual(self.test.address.route, "Somewhere Street") 279 | self.assertEqual(self.test.address.locality.name, "Northcote") 280 | self.assertEqual(self.test.address.locality.state.name, "Victoria") 281 | self.assertEqual(self.test.address.locality.state.code, "Victoria") 282 | self.assertEqual(self.test.address.locality.state.country.name, "Australia") 283 | 284 | def test_assignment_from_dict_invalid_country_code(self): 285 | ad = { 286 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 287 | "street_number": "1", 288 | "route": "Somewhere Street", 289 | "locality": "Northcote", 290 | "state": "Victoria", 291 | "country": "Australia", 292 | "country_code": "Something else", 293 | } 294 | self.assertRaises(ValueError, to_python, ad) 295 | 296 | def test_assignment_from_dict_invalid_state_code(self): 297 | ad = { 298 | "raw": "1 Somewhere Street, Northcote, Victoria 3070, VIC, AU", 299 | "street_number": "1", 300 | "route": "Somewhere Street", 301 | "locality": "Northcote", 302 | "state": "Victoria", 303 | "state_code": "Something", 304 | "country": "Australia", 305 | } 306 | # This is invalid because state codes are expected to have a max of 8 characters 307 | self.assertRaises(ValueError, to_python, ad) 308 | 309 | def test_assignment_from_string(self): 310 | self.test.address = to_python(self.ad1_dict["raw"]) 311 | self.assertEqual(self.test.address.raw, self.ad1_dict["raw"]) 312 | 313 | # def test_save(self): 314 | # self.test.address = self.ad1_dict 315 | # self.test.save() 316 | # test = self.TestModel.objects.all()[0] 317 | # self.assertEqual(test.address.raw, self.ad1_dict['raw']) 318 | # self.assertEqual(test.address.street_number, self.ad1_dict['street_number']) 319 | # self.assertEqual(test.address.route, self.ad1_dict['route']) 320 | # self.assertEqual(test.address.locality.name, self.ad1_dict['locality']) 321 | # self.assertEqual(test.address.locality.postal_code, self.ad1_dict['postal_code']) 322 | # self.assertEqual(test.address.locality.state.name, self.ad1_dict['state']) 323 | # self.assertEqual(test.address.locality.state.code, self.ad1_dict['state_code']) 324 | # self.assertEqual(test.address.locality.state.country.name, self.ad1_dict['country']) 325 | # self.assertEqual(test.address.locality.state.country.code, self.ad1_dict['country_code']) 326 | -------------------------------------------------------------------------------- /address/widgets.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django import forms 3 | from django.conf import settings 4 | from django.utils.html import escape 5 | from django.utils.safestring import mark_safe 6 | 7 | from .models import Address 8 | 9 | USE_DJANGO_JQUERY = getattr(settings, "USE_DJANGO_JQUERY", False) 10 | JQUERY_URL = getattr( 11 | settings, 12 | "JQUERY_URL", 13 | "https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js", 14 | ) 15 | 16 | 17 | class AddressWidget(forms.TextInput): 18 | components = [ 19 | ("country", "country"), 20 | ("country_code", "country_short"), 21 | ("locality", "locality"), 22 | ("sublocality", "sublocality"), 23 | ("postal_code", "postal_code"), 24 | ("postal_town", "postal_town"), 25 | ("route", "route"), 26 | ("street_number", "street_number"), 27 | ("state", "administrative_area_level_1"), 28 | ("state_code", "administrative_area_level_1_short"), 29 | ("formatted", "formatted_address"), 30 | ("latitude", "lat"), 31 | ("longitude", "lng"), 32 | ] 33 | 34 | class Media: 35 | """Media defined as a dynamic property instead of an inner class.""" 36 | 37 | js = [ 38 | "https://maps.googleapis.com/maps/api/js?libraries=places&key=%s" % settings.GOOGLE_API_KEY, 39 | "js/jquery.geocomplete.min.js", 40 | "address/js/address.js", 41 | ] 42 | 43 | if JQUERY_URL: 44 | js.insert(0, JQUERY_URL) 45 | elif JQUERY_URL is not False: 46 | vendor = "" if django.VERSION < (1, 9, 0) else "vendor/jquery/" 47 | extra = "" if settings.DEBUG else ".min" 48 | 49 | jquery_paths = [ 50 | "{}jquery{}.js".format(vendor, extra), 51 | "jquery.init.js", 52 | ] 53 | 54 | if USE_DJANGO_JQUERY: 55 | jquery_paths = ["admin/js/{}".format(path) for path in jquery_paths] 56 | 57 | js.extend(jquery_paths) 58 | 59 | def __init__(self, *args, **kwargs): 60 | attrs = kwargs.get("attrs", {}) 61 | classes = attrs.get("class", "") 62 | classes += (" " if classes else "") + "address" 63 | attrs["class"] = classes 64 | kwargs["attrs"] = attrs 65 | super(AddressWidget, self).__init__(*args, **kwargs) 66 | 67 | def render(self, name, value, attrs=None, **kwargs): 68 | 69 | # Can accept None, a dictionary of values or an Address object. 70 | if value in (None, ""): 71 | ad = {} 72 | elif isinstance(value, dict): 73 | ad = value 74 | elif isinstance(value, int): 75 | ad = Address.objects.get(pk=value) 76 | ad = ad.as_dict() 77 | else: 78 | ad = value.as_dict() 79 | 80 | # Generate the elements. We should create a suite of hidden fields 81 | # For each individual component, and a visible field for the raw 82 | # input. Begin by generating the raw input. 83 | elems = [super(AddressWidget, self).render(name, escape(ad.get("formatted", "")), attrs, **kwargs)] 84 | 85 | # Now add the hidden fields. 86 | elems.append('") 93 | 94 | return mark_safe("\n".join(elems)) 95 | 96 | def value_from_datadict(self, data, files, name): 97 | raw = data.get(name, "") 98 | if not raw: 99 | return raw 100 | ad = dict([(c[0], data.get(name + "_" + c[0], "")) for c in self.components]) 101 | ad["raw"] = raw 102 | return ad 103 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | db: 5 | image: postgres:13-alpine 6 | environment: 7 | - DATABASE_URL=postgres://postgres:postgres@localhost/postgres 8 | - POSTGRES_PASSWORD=postgres 9 | ports: 10 | - "5432:5432" 11 | restart: unless-stopped 12 | 13 | server: 14 | build: 15 | context: . 16 | dockerfile: ./Dockerfile 17 | args: 18 | - USER_ID 19 | - GROUP_ID 20 | environment: 21 | - DATABASE_URL=psql://postgres:postgres@db/postgres 22 | - GOOGLE_API_KEY 23 | volumes: 24 | - ./address:/code/address 25 | ports: 26 | - "8000:8000" 27 | depends_on: 28 | - db 29 | restart: unless-stopped 30 | stdin_open: true 31 | tty: true 32 | -------------------------------------------------------------------------------- /example_site/.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | -------------------------------------------------------------------------------- /example_site/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is a django demonstration project that shows how `django-address` can be used to geocode manually entered postal 4 | addresses. 5 | 6 | ## The Landing Page 7 | 8 | Screenshot of landing page 10 | 11 | ## The Admin View 12 | 13 | Screenshot of django admin 15 | 16 | ## The person app and Person model 17 | 18 | The person app is a simple implementation of a model called Person that includes an `AddressField` and a 19 | first_name. When you geocode an address using the main landing page of this app, it saves the name in an object with a 20 | blank first_name. 21 | 22 | You can use Django Admin to view the saved Person objects and add a first name if you like. 23 | 24 | ### Use of `null=true` on `address` of the Person model 25 | 26 | Note that the Person model uses Address field with `null=true`. 27 | 28 | By default, `django-address` uses Cascade delete on AddressField. 29 | 30 | This means if you for some reason delete an Address that is related to a Person or some other 31 | model, it will also delete the Person. 32 | 33 | By setting `null=True`, deleting the Address associated with a Person will keep the Person 34 | object instance and set `address` to null. 35 | 36 | # Setup 37 | 38 | If not already installed, [please install Docker](https://docs.docker.com/get-docker/). 39 | 40 | Before building the example site, you will need to export three items into your environment: 41 | 42 | ```bash 43 | export USER_ID=`id -u` 44 | export GROUP_ID=`id -g` 45 | export GOOGLE_API_KEY= 46 | ``` 47 | 48 | The first two are used by Docker to ensure any files created are owned by your current user. The last is required to 49 | make your Google Maps API key available to the example site. Instructions for setting up an API key here: [Google Maps 50 | API Key]. Please note that this requires the set up of a billing account with Google. 51 | 52 | ## Enable (activate) required Google Maps services for the project your key belongs to 53 | 54 | This is hidden under Google Cloud Platform's console menu, under **Other Google Solutions** > **Google Maps** > 55 | **APIs**. ([screenshot](https://user-images.githubusercontent.com/1409710/81484071-9d495580-91f7-11ea-891e-850fd5a225de.png)) 56 | * Google Maps _Javascript API_ 57 | * Google Maps _Places API_ 58 | 59 | ## Launch the server 60 | 61 | To run the example site, simply run: 62 | 63 | ```bash 64 | docker-compose up 65 | ``` 66 | 67 | This will take care of launching a database, the server, and migrating the database. 68 | 69 | ## Create a super admin to see results in Django Admin 70 | 71 | ```bash 72 | docker-compose run --rm server python manage.py createsuperuser 73 | ``` 74 | 75 | # The Project 76 | 77 | The page shows a simple form entry field. 78 | 79 | ### Troubleshooting Google Maps 80 | 81 | Check the browser console on the page for javascript errors. ([Screenshot of an error](https://user-images.githubusercontent.com/1409710/81484063-90c4fd00-91f7-11ea-8833-80a346c77f89.png)) 82 | * `ApiTargetBlockedMapError`: Your API key [needs authorization](https://developers.google.com/maps/documentation/javascript/error-messages#api-target-blocked-map-error) to use the above services. 83 | * `ApiNotActivatedMapError`: Your API key [needs Google Maps services](https://developers.google.com/maps/documentation/javascript/error-messages#api-target-blocked-map-error) to use the above services. 84 | 85 | ***NOTE:** There is up to a several minute delay in making changes to project and api key settings. New keys can also take several minutes to be recognized. 86 | 87 | [Google Maps API Key]: https://developers.google.com/maps/documentation/javascript/get-api-key 88 | [settings.py]: example_site/settings.py 89 | -------------------------------------------------------------------------------- /example_site/example_site/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-luke/django-address/6e5b13859b8a795b08189dde7ce1aab4cca18827/example_site/example_site/__init__.py -------------------------------------------------------------------------------- /example_site/example_site/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example_site project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.dev20150302062936. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | import environ 17 | 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | 20 | env = environ.Env() 21 | 22 | 23 | # Quick-start development settings - unsuitable for production 24 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 25 | 26 | # SECURITY WARNING: keep the secret key used in production secret! 27 | SECRET_KEY = "fbaa1unu0e8z5@9mm%k#+*d@iny*=-)ma2b#ymq)o9z^3%ijh)" 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = True 31 | 32 | TEMPLATE_DEBUG = True 33 | 34 | ALLOWED_HOSTS = [] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = ( 40 | "address", 41 | "person", 42 | "django.contrib.admin", 43 | "django.contrib.auth", 44 | "django.contrib.contenttypes", 45 | "django.contrib.sessions", 46 | "django.contrib.messages", 47 | "django.contrib.staticfiles", 48 | ) 49 | 50 | MIDDLEWARE = ( 51 | "django.middleware.security.SecurityMiddleware", 52 | "django.contrib.sessions.middleware.SessionMiddleware", 53 | "django.middleware.common.CommonMiddleware", 54 | "django.middleware.csrf.CsrfViewMiddleware", 55 | "django.contrib.auth.middleware.AuthenticationMiddleware", 56 | "django.contrib.messages.middleware.MessageMiddleware", 57 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 58 | ) 59 | 60 | ROOT_URLCONF = "example_site.urls" 61 | 62 | TEMPLATES = [ 63 | { 64 | "BACKEND": "django.template.backends.django.DjangoTemplates", 65 | "DIRS": [], 66 | "APP_DIRS": True, 67 | "OPTIONS": { 68 | "context_processors": [ 69 | "django.template.context_processors.debug", 70 | "django.template.context_processors.request", 71 | "django.contrib.auth.context_processors.auth", 72 | "django.contrib.messages.context_processors.messages", 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = "example_site.wsgi.application" 79 | 80 | # Specify your Google API key as environment variable GOOGLE_API_KEY 81 | # You may also specify it here, though be sure not to commit it to a repository 82 | GOOGLE_API_KEY = "" # Specify your Google API key here 83 | GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", GOOGLE_API_KEY) 84 | 85 | # Database 86 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 87 | 88 | DATABASES = { 89 | "default": env.db(), 90 | } 91 | 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 99 | }, 100 | { 101 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 102 | }, 103 | { 104 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 105 | }, 106 | { 107 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 108 | }, 109 | ] 110 | 111 | 112 | # Internationalization 113 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 114 | 115 | LANGUAGE_CODE = "en-us" 116 | 117 | TIME_ZONE = "UTC" 118 | 119 | USE_I18N = True 120 | 121 | USE_L10N = True 122 | 123 | USE_TZ = True 124 | 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 128 | 129 | STATIC_URL = "/static/" 130 | 131 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 132 | -------------------------------------------------------------------------------- /example_site/example_site/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | from person import views as person 5 | 6 | urlpatterns = [ 7 | path("", person.home, name="home"), 8 | path("admin/", admin.site.urls), 9 | ] 10 | -------------------------------------------------------------------------------- /example_site/example_site/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example_site project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_site.settings") 13 | 14 | from django.core.wsgi import get_wsgi_application # noqa 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example_site/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", "example_site.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example_site/person/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-luke/django-address/6e5b13859b8a795b08189dde7ce1aab4cca18827/example_site/person/__init__.py -------------------------------------------------------------------------------- /example_site/person/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from address.models import AddressField 3 | from address.forms import AddressWidget 4 | from .models import Person 5 | 6 | 7 | @admin.register(Person) 8 | class PersonAdmin(admin.ModelAdmin): 9 | 10 | list_display = ( 11 | "id", 12 | "first_name", 13 | "address", 14 | ) 15 | 16 | formfield_overrides = {AddressField: {"widget": AddressWidget(attrs={"style": "width: 300px;"})}} 17 | -------------------------------------------------------------------------------- /example_site/person/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PersonConfig(AppConfig): 5 | name = "person" 6 | -------------------------------------------------------------------------------- /example_site/person/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from address.forms import AddressField 3 | from .models import Person 4 | 5 | 6 | class PersonForm(forms.ModelForm): 7 | class Meta: 8 | model = Person 9 | address = AddressField() 10 | fields = "__all__" 11 | -------------------------------------------------------------------------------- /example_site/person/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.3 on 2018-03-29 22:34 2 | 3 | import address.models 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("address", "0002_auto_20160213_1726"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Person", 19 | fields=[ 20 | ( 21 | "id", 22 | models.AutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ( 30 | "address", 31 | address.models.AddressField( 32 | on_delete=django.db.models.deletion.CASCADE, 33 | to="address.Address", 34 | ), 35 | ), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /example_site/person/migrations/0002_auto_20200628_1720.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-28 17:20 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("person", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="person", 15 | options={"verbose_name": "Person", "verbose_name_plural": "People"}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /example_site/person/migrations/0003_auto_20200628_1920.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-06-28 19:20 2 | 3 | import address.models 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ("address", "0002_auto_20160213_1726"), 12 | ("person", "0002_auto_20200628_1720"), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name="person", 18 | name="first_name", 19 | field=models.CharField(blank=True, max_length=20), 20 | ), 21 | migrations.AlterField( 22 | model_name="person", 23 | name="address", 24 | field=address.models.AddressField( 25 | blank=True, 26 | null=True, 27 | on_delete=django.db.models.deletion.SET_NULL, 28 | to="address.Address", 29 | ), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /example_site/person/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-luke/django-address/6e5b13859b8a795b08189dde7ce1aab4cca18827/example_site/person/migrations/__init__.py -------------------------------------------------------------------------------- /example_site/person/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from address.models import AddressField 3 | 4 | 5 | class Person(models.Model): 6 | """Model definition for Person.""" 7 | 8 | first_name = models.CharField(max_length=20, blank=True) 9 | address = AddressField(null=True, blank=True) 10 | 11 | class Meta: 12 | """Meta definition for Person.""" 13 | 14 | verbose_name = "Person" 15 | verbose_name_plural = "People" 16 | 17 | def __str__(self): 18 | """Unicode representation of Person.""" 19 | return "%s" % self.id 20 | -------------------------------------------------------------------------------- /example_site/person/templates/example/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | django-address 8 | 9 | 22 | 23 | 24 | {{ form.media }} 25 | 26 | 27 | {% if not google_api_key_set %} 28 |

This django-address example site is running correctly. 29 |

However, you must set GOOGLE_API_KEY in settings.py to use this demo.

30 |

See this sample project's readme for instructions.

31 | {% else %} 32 |

django-address demo application

33 |
34 | {% csrf_token %} 35 | {{ form }} 36 | 37 |
38 | {% if success %} 39 |

Successfully submitted an address.

40 |

View address model data in django admin.

41 | {% endif %} 42 |

Total addresses saved: {{ addresses.count }}

43 | {% endif %} 44 | 45 | 46 | -------------------------------------------------------------------------------- /example_site/person/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import render 3 | 4 | from address.models import Address 5 | from .forms import PersonForm 6 | 7 | 8 | def home(request): 9 | success = False 10 | addresses = Address.objects.all() 11 | if settings.GOOGLE_API_KEY: 12 | google_api_key_set = True 13 | else: 14 | google_api_key_set = False 15 | 16 | if request.method == "POST": 17 | form = PersonForm(request.POST) 18 | if form.is_valid(): 19 | success = True 20 | form.save() 21 | else: 22 | form = PersonForm(initial={"address": Address.objects.last()}) 23 | 24 | context = { 25 | "form": form, 26 | "google_api_key_set": google_api_key_set, 27 | "success": success, 28 | "addresses": addresses, 29 | } 30 | 31 | return render(request, "example/home.html", context) 32 | -------------------------------------------------------------------------------- /example_site/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "asgiref" 3 | version = "3.4.1" 4 | description = "ASGI specs, helper code, and adapters" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 11 | 12 | [package.extras] 13 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 14 | 15 | [[package]] 16 | name = "backports.entry-points-selectable" 17 | version = "1.1.0" 18 | description = "Compatibility shim providing selectable entry points for older implementations" 19 | category = "dev" 20 | optional = false 21 | python-versions = ">=2.7" 22 | 23 | [package.dependencies] 24 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 25 | 26 | [package.extras] 27 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 28 | testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 29 | 30 | [[package]] 31 | name = "colorama" 32 | version = "0.4.4" 33 | description = "Cross-platform colored terminal text." 34 | category = "dev" 35 | optional = false 36 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 37 | 38 | [[package]] 39 | name = "distlib" 40 | version = "0.3.2" 41 | description = "Distribution utilities" 42 | category = "dev" 43 | optional = false 44 | python-versions = "*" 45 | 46 | [[package]] 47 | name = "django" 48 | version = "3.2.6" 49 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 50 | category = "main" 51 | optional = false 52 | python-versions = ">=3.6" 53 | 54 | [package.dependencies] 55 | asgiref = ">=3.3.2,<4" 56 | pytz = "*" 57 | sqlparse = ">=0.2.2" 58 | 59 | [package.extras] 60 | argon2 = ["argon2-cffi (>=19.1.0)"] 61 | bcrypt = ["bcrypt"] 62 | 63 | [[package]] 64 | name = "django-environ" 65 | version = "0.4.5" 66 | description = "Django-environ allows you to utilize 12factor inspired environment variables to configure your Django application." 67 | category = "main" 68 | optional = false 69 | python-versions = "*" 70 | 71 | [[package]] 72 | name = "filelock" 73 | version = "3.0.12" 74 | description = "A platform independent file lock." 75 | category = "dev" 76 | optional = false 77 | python-versions = "*" 78 | 79 | [[package]] 80 | name = "importlib-metadata" 81 | version = "4.6.4" 82 | description = "Read metadata from Python packages" 83 | category = "dev" 84 | optional = false 85 | python-versions = ">=3.6" 86 | 87 | [package.dependencies] 88 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 89 | zipp = ">=0.5" 90 | 91 | [package.extras] 92 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 93 | perf = ["ipython"] 94 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 95 | 96 | [[package]] 97 | name = "importlib-resources" 98 | version = "5.2.2" 99 | description = "Read resources from Python packages" 100 | category = "dev" 101 | optional = false 102 | python-versions = ">=3.6" 103 | 104 | [package.dependencies] 105 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 106 | 107 | [package.extras] 108 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 109 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] 110 | 111 | [[package]] 112 | name = "packaging" 113 | version = "21.0" 114 | description = "Core utilities for Python packages" 115 | category = "dev" 116 | optional = false 117 | python-versions = ">=3.6" 118 | 119 | [package.dependencies] 120 | pyparsing = ">=2.0.2" 121 | 122 | [[package]] 123 | name = "platformdirs" 124 | version = "2.2.0" 125 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 126 | category = "dev" 127 | optional = false 128 | python-versions = ">=3.6" 129 | 130 | [package.extras] 131 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 132 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 133 | 134 | [[package]] 135 | name = "pluggy" 136 | version = "0.13.1" 137 | description = "plugin and hook calling mechanisms for python" 138 | category = "dev" 139 | optional = false 140 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 141 | 142 | [package.dependencies] 143 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 144 | 145 | [package.extras] 146 | dev = ["pre-commit", "tox"] 147 | 148 | [[package]] 149 | name = "psycopg2-binary" 150 | version = "2.9.1" 151 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 152 | category = "main" 153 | optional = false 154 | python-versions = ">=3.6" 155 | 156 | [[package]] 157 | name = "py" 158 | version = "1.10.0" 159 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 160 | category = "dev" 161 | optional = false 162 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 163 | 164 | [[package]] 165 | name = "pyparsing" 166 | version = "2.4.7" 167 | description = "Python parsing module" 168 | category = "dev" 169 | optional = false 170 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 171 | 172 | [[package]] 173 | name = "pytz" 174 | version = "2021.1" 175 | description = "World timezone definitions, modern and historical" 176 | category = "main" 177 | optional = false 178 | python-versions = "*" 179 | 180 | [[package]] 181 | name = "six" 182 | version = "1.16.0" 183 | description = "Python 2 and 3 compatibility utilities" 184 | category = "dev" 185 | optional = false 186 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 187 | 188 | [[package]] 189 | name = "sqlparse" 190 | version = "0.4.1" 191 | description = "A non-validating SQL parser." 192 | category = "main" 193 | optional = false 194 | python-versions = ">=3.5" 195 | 196 | [[package]] 197 | name = "toml" 198 | version = "0.10.2" 199 | description = "Python Library for Tom's Obvious, Minimal Language" 200 | category = "dev" 201 | optional = false 202 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 203 | 204 | [[package]] 205 | name = "tox" 206 | version = "3.24.3" 207 | description = "tox is a generic virtualenv management and test command line tool" 208 | category = "dev" 209 | optional = false 210 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 211 | 212 | [package.dependencies] 213 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 214 | filelock = ">=3.0.0" 215 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 216 | packaging = ">=14" 217 | pluggy = ">=0.12.0" 218 | py = ">=1.4.17" 219 | six = ">=1.14.0" 220 | toml = ">=0.9.4" 221 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 222 | 223 | [package.extras] 224 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 225 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] 226 | 227 | [[package]] 228 | name = "typing-extensions" 229 | version = "3.10.0.0" 230 | description = "Backported and Experimental Type Hints for Python 3.5+" 231 | category = "main" 232 | optional = false 233 | python-versions = "*" 234 | 235 | [[package]] 236 | name = "virtualenv" 237 | version = "20.7.2" 238 | description = "Virtual Python Environment builder" 239 | category = "dev" 240 | optional = false 241 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 242 | 243 | [package.dependencies] 244 | "backports.entry-points-selectable" = ">=1.0.4" 245 | distlib = ">=0.3.1,<1" 246 | filelock = ">=3.0.0,<4" 247 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 248 | importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} 249 | platformdirs = ">=2,<3" 250 | six = ">=1.9.0,<2" 251 | 252 | [package.extras] 253 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 254 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 255 | 256 | [[package]] 257 | name = "zipp" 258 | version = "3.5.0" 259 | description = "Backport of pathlib-compatible object wrapper for zip files" 260 | category = "dev" 261 | optional = false 262 | python-versions = ">=3.6" 263 | 264 | [package.extras] 265 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 266 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 267 | 268 | [metadata] 269 | lock-version = "1.1" 270 | python-versions = ">=3.6" 271 | content-hash = "b32c7b293cdc5a3554b28ae3115a00b59caa4512439aa58253b5e9b328734158" 272 | 273 | [metadata.files] 274 | asgiref = [ 275 | {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, 276 | {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, 277 | ] 278 | "backports.entry-points-selectable" = [ 279 | {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, 280 | {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, 281 | ] 282 | colorama = [ 283 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 284 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 285 | ] 286 | distlib = [ 287 | {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, 288 | {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, 289 | ] 290 | django = [ 291 | {file = "Django-3.2.6-py3-none-any.whl", hash = "sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13"}, 292 | {file = "Django-3.2.6.tar.gz", hash = "sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022"}, 293 | ] 294 | django-environ = [ 295 | {file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"}, 296 | {file = "django_environ-0.4.5-py2.py3-none-any.whl", hash = "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"}, 297 | ] 298 | filelock = [ 299 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 300 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 301 | ] 302 | importlib-metadata = [ 303 | {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"}, 304 | {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"}, 305 | ] 306 | importlib-resources = [ 307 | {file = "importlib_resources-5.2.2-py3-none-any.whl", hash = "sha256:2480d8e07d1890056cb53c96e3de44fead9c62f2ba949b0f2e4c4345f4afa977"}, 308 | {file = "importlib_resources-5.2.2.tar.gz", hash = "sha256:a65882a4d0fe5fbf702273456ba2ce74fe44892c25e42e057aca526b702a6d4b"}, 309 | ] 310 | packaging = [ 311 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 312 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 313 | ] 314 | platformdirs = [ 315 | {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, 316 | {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, 317 | ] 318 | pluggy = [ 319 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 320 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 321 | ] 322 | psycopg2-binary = [ 323 | {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, 324 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, 325 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, 326 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, 327 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7"}, 328 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"}, 329 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d"}, 330 | {file = "psycopg2_binary-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce"}, 331 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a"}, 332 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a"}, 333 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0"}, 334 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed"}, 335 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f"}, 336 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e"}, 337 | {file = "psycopg2_binary-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2"}, 338 | {file = "psycopg2_binary-2.9.1-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140"}, 339 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917"}, 340 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90"}, 341 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72"}, 342 | {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34"}, 343 | {file = "psycopg2_binary-2.9.1-cp38-cp38-win32.whl", hash = "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32"}, 344 | {file = "psycopg2_binary-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf"}, 345 | {file = "psycopg2_binary-2.9.1-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a"}, 346 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4"}, 347 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31"}, 348 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd"}, 349 | {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a"}, 350 | {file = "psycopg2_binary-2.9.1-cp39-cp39-win32.whl", hash = "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975"}, 351 | {file = "psycopg2_binary-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68"}, 352 | ] 353 | py = [ 354 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 355 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 356 | ] 357 | pyparsing = [ 358 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 359 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 360 | ] 361 | pytz = [ 362 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 363 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 364 | ] 365 | six = [ 366 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 367 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 368 | ] 369 | sqlparse = [ 370 | {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, 371 | {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, 372 | ] 373 | toml = [ 374 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 375 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 376 | ] 377 | tox = [ 378 | {file = "tox-3.24.3-py2.py3-none-any.whl", hash = "sha256:9fbf8e2ab758b2a5e7cb2c72945e4728089934853076f67ef18d7575c8ab6b88"}, 379 | {file = "tox-3.24.3.tar.gz", hash = "sha256:c6c4e77705ada004283610fd6d9ba4f77bc85d235447f875df9f0ba1bc23b634"}, 380 | ] 381 | typing-extensions = [ 382 | {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, 383 | {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, 384 | {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, 385 | ] 386 | virtualenv = [ 387 | {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, 388 | {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, 389 | ] 390 | zipp = [ 391 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 392 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 393 | ] 394 | -------------------------------------------------------------------------------- /example_site/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "example_site" 3 | version = "0.1.0" 4 | description = "An example site for django-address." 5 | authors = ["Luke Hodkinson "] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.6" 9 | Django = ">=2.1" 10 | django-environ = "^0.4.5" 11 | psycopg2-binary = "^2.9.1" 12 | 13 | [tool.poetry.dev-dependencies] 14 | tox = "^3.24.2" 15 | 16 | [build-system] 17 | requires = ["poetry-core>=1.0.0"] 18 | build-backend = "poetry.core.masonry.api" 19 | -------------------------------------------------------------------------------- /example_site/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | -------------------------------------------------------------------------------- /example_site/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = true 3 | envlist = py35,py39 4 | 5 | [testenv] 6 | whitelist_externals = poetry 7 | setenv = 8 | DATABASE_URL = {env:DATABASE_URL:postgres://postgres:postgres@localhost/postgres} 9 | commands = 10 | poetry install 11 | poetry run python manage.py test 12 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "black" 11 | version = "21.7b0" 12 | description = "The uncompromising code formatter." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=3.6.2" 16 | 17 | [package.dependencies] 18 | appdirs = "*" 19 | click = ">=7.1.2" 20 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} 21 | mypy-extensions = ">=0.4.3" 22 | pathspec = ">=0.8.1,<1" 23 | regex = ">=2020.1.8" 24 | tomli = ">=0.2.6,<2.0.0" 25 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} 26 | typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} 27 | 28 | [package.extras] 29 | colorama = ["colorama (>=0.4.3)"] 30 | d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] 31 | python2 = ["typed-ast (>=1.4.2)"] 32 | uvloop = ["uvloop (>=0.15.2)"] 33 | 34 | [[package]] 35 | name = "click" 36 | version = "8.0.1" 37 | description = "Composable command line interface toolkit" 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=3.6" 41 | 42 | [package.dependencies] 43 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 44 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 45 | 46 | [[package]] 47 | name = "colorama" 48 | version = "0.4.4" 49 | description = "Cross-platform colored terminal text." 50 | category = "dev" 51 | optional = false 52 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 53 | 54 | [[package]] 55 | name = "dataclasses" 56 | version = "0.8" 57 | description = "A backport of the dataclasses module for Python 3.6" 58 | category = "dev" 59 | optional = false 60 | python-versions = ">=3.6, <3.7" 61 | 62 | [[package]] 63 | name = "django" 64 | version = "2.2.24" 65 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 66 | category = "main" 67 | optional = false 68 | python-versions = ">=3.5" 69 | 70 | [package.dependencies] 71 | pytz = "*" 72 | sqlparse = ">=0.2.2" 73 | 74 | [package.extras] 75 | argon2 = ["argon2-cffi (>=16.1.0)"] 76 | bcrypt = ["bcrypt"] 77 | 78 | [[package]] 79 | name = "flake8" 80 | version = "3.9.2" 81 | description = "the modular source code checker: pep8 pyflakes and co" 82 | category = "dev" 83 | optional = false 84 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 85 | 86 | [package.dependencies] 87 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 88 | mccabe = ">=0.6.0,<0.7.0" 89 | pycodestyle = ">=2.7.0,<2.8.0" 90 | pyflakes = ">=2.3.0,<2.4.0" 91 | 92 | [[package]] 93 | name = "importlib-metadata" 94 | version = "4.6.4" 95 | description = "Read metadata from Python packages" 96 | category = "dev" 97 | optional = false 98 | python-versions = ">=3.6" 99 | 100 | [package.dependencies] 101 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 102 | zipp = ">=0.5" 103 | 104 | [package.extras] 105 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 106 | perf = ["ipython"] 107 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 108 | 109 | [[package]] 110 | name = "mccabe" 111 | version = "0.6.1" 112 | description = "McCabe checker, plugin for flake8" 113 | category = "dev" 114 | optional = false 115 | python-versions = "*" 116 | 117 | [[package]] 118 | name = "mypy-extensions" 119 | version = "0.4.3" 120 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 121 | category = "dev" 122 | optional = false 123 | python-versions = "*" 124 | 125 | [[package]] 126 | name = "pathspec" 127 | version = "0.9.0" 128 | description = "Utility library for gitignore style pattern matching of file paths." 129 | category = "dev" 130 | optional = false 131 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 132 | 133 | [[package]] 134 | name = "pycodestyle" 135 | version = "2.7.0" 136 | description = "Python style guide checker" 137 | category = "dev" 138 | optional = false 139 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 140 | 141 | [[package]] 142 | name = "pyflakes" 143 | version = "2.3.1" 144 | description = "passive checker of Python programs" 145 | category = "dev" 146 | optional = false 147 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 148 | 149 | [[package]] 150 | name = "pytz" 151 | version = "2021.1" 152 | description = "World timezone definitions, modern and historical" 153 | category = "main" 154 | optional = false 155 | python-versions = "*" 156 | 157 | [[package]] 158 | name = "regex" 159 | version = "2021.8.3" 160 | description = "Alternative regular expression module, to replace re." 161 | category = "dev" 162 | optional = false 163 | python-versions = "*" 164 | 165 | [[package]] 166 | name = "sqlparse" 167 | version = "0.4.1" 168 | description = "A non-validating SQL parser." 169 | category = "main" 170 | optional = false 171 | python-versions = ">=3.5" 172 | 173 | [[package]] 174 | name = "tomli" 175 | version = "1.2.1" 176 | description = "A lil' TOML parser" 177 | category = "dev" 178 | optional = false 179 | python-versions = ">=3.6" 180 | 181 | [[package]] 182 | name = "typed-ast" 183 | version = "1.4.3" 184 | description = "a fork of Python 2 and 3 ast modules with type comment support" 185 | category = "dev" 186 | optional = false 187 | python-versions = "*" 188 | 189 | [[package]] 190 | name = "typing-extensions" 191 | version = "3.10.0.0" 192 | description = "Backported and Experimental Type Hints for Python 3.5+" 193 | category = "dev" 194 | optional = false 195 | python-versions = "*" 196 | 197 | [[package]] 198 | name = "zipp" 199 | version = "3.5.0" 200 | description = "Backport of pathlib-compatible object wrapper for zip files" 201 | category = "dev" 202 | optional = false 203 | python-versions = ">=3.6" 204 | 205 | [package.extras] 206 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 207 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 208 | 209 | [metadata] 210 | lock-version = "1.1" 211 | python-versions = ">=3.5" 212 | content-hash = "21552d6094ac5d9a6b843ec9e70c0fa5f9874bc7820469b2744112eac47b2206" 213 | 214 | [metadata.files] 215 | appdirs = [ 216 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 217 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 218 | ] 219 | black = [ 220 | {file = "black-21.7b0-py3-none-any.whl", hash = "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116"}, 221 | {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"}, 222 | ] 223 | click = [ 224 | {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, 225 | {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, 226 | ] 227 | colorama = [ 228 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 229 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 230 | ] 231 | dataclasses = [ 232 | {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, 233 | {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, 234 | ] 235 | django = [ 236 | {file = "Django-2.2.24-py3-none-any.whl", hash = "sha256:f2084ceecff86b1e631c2cd4107d435daf4e12f1efcdf11061a73bf0b5e95f92"}, 237 | {file = "Django-2.2.24.tar.gz", hash = "sha256:3339ff0e03dee13045aef6ae7b523edff75b6d726adf7a7a48f53d5a501f7db7"}, 238 | ] 239 | flake8 = [ 240 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 241 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 242 | ] 243 | importlib-metadata = [ 244 | {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"}, 245 | {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"}, 246 | ] 247 | mccabe = [ 248 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 249 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 250 | ] 251 | mypy-extensions = [ 252 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 253 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 254 | ] 255 | pathspec = [ 256 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 257 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 258 | ] 259 | pycodestyle = [ 260 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 261 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 262 | ] 263 | pyflakes = [ 264 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 265 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 266 | ] 267 | pytz = [ 268 | {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, 269 | {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, 270 | ] 271 | regex = [ 272 | {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, 273 | {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, 274 | {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, 275 | {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, 276 | {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, 277 | {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, 278 | {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, 279 | {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, 280 | {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, 281 | {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, 282 | {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, 283 | {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, 284 | {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, 285 | {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, 286 | {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, 287 | {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, 288 | {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, 289 | {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, 290 | {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, 291 | {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, 292 | {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, 293 | {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, 294 | {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, 295 | {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, 296 | {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, 297 | {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, 298 | {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, 299 | {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, 300 | {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, 301 | {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, 302 | {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, 303 | {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, 304 | {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, 305 | ] 306 | sqlparse = [ 307 | {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, 308 | {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, 309 | ] 310 | tomli = [ 311 | {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, 312 | {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, 313 | ] 314 | typed-ast = [ 315 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 316 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 317 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 318 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 319 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 320 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 321 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 322 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 323 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 324 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 325 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 326 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 327 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 328 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 329 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 330 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 331 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 332 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 333 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 334 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 335 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 336 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 337 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 338 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 339 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 340 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 341 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 342 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 343 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 344 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 345 | ] 346 | typing-extensions = [ 347 | {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, 348 | {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, 349 | {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, 350 | ] 351 | zipp = [ 352 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 353 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 354 | ] 355 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-address" 3 | version = "0.2.5" 4 | description = "A django application for describing addresses." 5 | authors = ["Luke Hodkinson "] 6 | license = "BSD" 7 | readme = "README.md" 8 | homepage = "https://github.com/furious-luke/django-address" 9 | repository = "https://github.com/furious-luke/django-address" 10 | classifiers = [ 11 | "Development Status :: 3 - Alpha", 12 | "Framework :: Django", 13 | "Framework :: Django :: 2.2", 14 | "Framework :: Django :: 3.0", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: BSD License", 17 | "Natural Language :: English", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3.5", 21 | "Programming Language :: Python :: 3.6", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8" 24 | ] 25 | packages = [ 26 | { include = "address" } 27 | ] 28 | include = ["setup.cfg"] 29 | 30 | [tool.poetry.dependencies] 31 | python = ">=3.5" 32 | Django = ">=2.1" 33 | 34 | [tool.poetry.dev-dependencies] 35 | black = {version = ">=21.7b0", python = ">=3.6.2"} 36 | flake8 = "^3.9.2" 37 | 38 | [tool.black] 39 | line-length = 119 40 | 41 | [build-system] 42 | requires = ["poetry-core>=1.0.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import find_packages, setup 5 | 6 | version = "0.2.5" 7 | 8 | if sys.argv[-1] == "tag": 9 | print("Tagging the version on github:") 10 | os.system("git tag -a %s -m 'version %s'" % (version, version)) 11 | os.system("git push --tags") 12 | sys.exit() 13 | 14 | setup( 15 | name="django-address", 16 | version=version, 17 | author="Luke Hodkinson", 18 | author_email="furious.luke@gmail.com", 19 | maintainer="Rob Banagale", 20 | maintainer_email="rob@banagale.com", 21 | url="https://github.com/furious-luke/django-address", 22 | description="A django application for describing addresses.", 23 | long_description=open(os.path.join(os.path.dirname(__file__), "README.md")).read(), 24 | long_description_content_type="text/markdown", 25 | classifiers=[ 26 | "Development Status :: 3 - Alpha", 27 | "Framework :: Django", 28 | "Framework :: Django :: 2.2", 29 | "Framework :: Django :: 3.0", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: BSD License", 32 | "Natural Language :: English", 33 | "Operating System :: OS Independent", 34 | "Programming Language :: Python", 35 | "Programming Language :: Python :: 3.5", 36 | "Programming Language :: Python :: 3.6", 37 | "Programming Language :: Python :: 3.7", 38 | "Programming Language :: Python :: 3.8", 39 | ], 40 | license="BSD", 41 | packages=find_packages(), 42 | include_package_data=True, 43 | package_data={"": ["*.txt", "*.js", "*.html", "*.*"]}, 44 | install_requires=["setuptools"], 45 | zip_safe=False, 46 | ) 47 | --------------------------------------------------------------------------------