├── .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('' % name)
87 | for com in self.components:
88 | elems.append(
89 | ''
90 | % (name, com[0], com[1], escape(ad.get(com[0], "")))
91 | )
92 | 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 |
10 |
11 | ## The Admin View
12 |
13 |
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 |
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 |
--------------------------------------------------------------------------------